"""
闲鱼API管理模块
集成搜索、下单、收货地址等API，包含完整的风控、登录过期、token过期处理逻辑
参考: csharp/ApiRequest/GoofishApi/ApiRequest.cs
"""

import asyncio
import hashlib
import json
import time
from typing import Dict, Any
from urllib.parse import quote

import aiohttp
from loguru import logger

from modules.risk_api import RiskAPI


class ResponseCode:
    """API响应码"""
    SUCCESS = "SUCCESS"
    FAIL_SYS_TOKEN_EMPTY = "FAIL_SYS_TOKEN_EMPTY" # _m_h5_tk 不存在，需要重新获取token
    FAIL_SYS_TOKEN_EXPIRED = "FAIL_SYS_TOKEN_EXPIRED" # _m_h5_tk 过期，需要重新获取token（兼容拼写错误的 EXOIRED）
    FAIL_SYS_TOKEN_ILLEGAL = "FAIL_SYS_TOKEN_ILLEGAL" # _m_h5_tk 无效或非法（被修改），需要重新获取token
    FAIL_SYS_SESSION_EXPIRED = "FAIL_SYS_SESSION_EXPIRED" # 登录过期，需要重新登录
    # 非法请求（token签名错误、时间戳过期、cookie不匹配等）
    FAIL_SYS_ILLEGAL_ACCESS = "FAIL_SYS_ILLEGAL_ACCESS" # 非法请求，必须停止操作
    # 风控标志
    RGV587_ERROR = "RGV587"  
    # 用户验证，一般和RGV587_ERROR一起出现
    FAIL_SYS_USER_VALIDATE = "FAIL_SYS_USER_VALIDATE"
    # PC不支持此类型交易，请去闲鱼app下单
    FAIL_BIZ_PC_CAN_NOT_TRADE = "FAIL_BIZ_PC_CAN_NOT_TRADE"
    # 库存被锁定（有人拍下但未付款）
    FAIL_BIZ_ALL_INVENTORY_WITH_HOLDING = "FAIL_BIZ_ALL_INVENTORY_WITH_HOLDING"
    # 当前商品无法购买
    FAIL_BIZ_ITEM_CANNOT_BUY_ERROR = "FAIL_BIZ_ITEM_CANNOT_BUY_ERROR"
    # 商品库存不足（已抢光）
    FAIL_BIZ_DEFAULT_INVENTORY_NOT_ENOUGH = "FAIL_BIZ_DEFAULT_INVENTORY_NOT_ENOUGH"
    FAIL_SYS_REQUEST_TIMEOUT = "FAIL_SYS_REQUEST_TIMEOUT"

class APIDefinition:
    """API定义"""
    def __init__(self, api_name: str, version: str = "1.0", spm_cnt: str = "", spm_pre: str = "", format_type: str = "originaljson"):
        self.api_name = api_name
        self.version = version
        self.spm_cnt = spm_cnt
        self.spm_pre = spm_pre
        self.format_type = format_type
        self.data_type = "json"


class GoofishAPIs:
    """闲鱼API预设"""
    
    # 首页索引（用于获取token）
    HOME_INDEX = APIDefinition(
        api_name="mtop.gaia.nodejs.gaia.idle.data.gw.v2.index.get",
        spm_cnt="a21ybx.home.0.0"
    )
    
    # 搜索
    SEARCH = APIDefinition(
        api_name="mtop.taobao.idlemtopsearch.pc.search",
        spm_cnt="a21ybx.search.0.0",
        spm_pre="a21ybx.search.searchInput.0"
    )
    
    # 收货地址列表
    ADDRESS_LIST = APIDefinition(
        api_name="mtop.taobao.idle.logistic.address.list.query",
        spm_cnt="a21ybx.create-order.0.0",
        spm_pre="a21ybx.item.buy.1.bf253da6Szegq8"
    )
    
    # 获取登录用户信息
    LOGIN_USER_GET = APIDefinition(
        api_name="mtop.taobao.idlemessage.pc.loginuser.get",
        spm_cnt="a21ybx.personal.0.0",
        spm_pre="a21ybx.personal.0.0"
    )
    
    # 订单渲染（获取订单信息）
    ORDER_RENDER = APIDefinition(
        api_name="mtop.taobao.idle.trade.order.render",
        version="7.0",
        spm_cnt="a21ybx.order-detail.0.0",
        format_type="json"
    )
    
    # 创建订单
    CREATE_ORDER = APIDefinition(
        api_name="mtop.taobao.idle.trade.order.create",
        version="5.0",
        spm_cnt="a21ybx.create-order.0.0",
        format_type="json"
    )
    
    # 商品详情
    ITEM_DETAIL = APIDefinition(
        api_name="mtop.taobao.idle.pc.detail",
        spm_cnt="a21ybx.item.0.0"
    )
    
    # 取消订单
    CLOSE_ORDER = APIDefinition(
        api_name="mtop.taobao.idle.trade.order.cancel",
        version="2.0",
        spm_cnt="a21ybx.bought.0.0",
        spm_pre="a21ybx.create-order.nav.2"
    )
    
    # 查询黑名单
    BLACKLIST_QUERY = APIDefinition(
        api_name="mtop.taobao.idlemessage.pc.blacklist.query",
        version="1.0",
        spm_cnt="a21ybx.im.0.0"
    )
    
    # 添加黑名单
    BLACKLIST_ADD = APIDefinition(
        api_name="mtop.taobao.idlemessage.pc.blacklist.add",
        version="2.0",
        spm_cnt="a21ybx.im.0.0"
    )
    
    # 移除黑名单
    BLACKLIST_REMOVE = APIDefinition(
        api_name="mtop.taobao.idlemessage.pc.blacklist.remove",
        version="1.0",
        spm_cnt="a21ybx.im.0.0"
    )
    
    # 加载表情包列表
    EMOJI_LOAD = APIDefinition(
        api_name="mtop.taobao.idlemessage.face.emoji.load",
        version="1.0",
        spm_cnt="a21ybx.im.0.0"
    )


class GoofishAPIClient:
    """闲鱼API客户端"""
    
    HOST_URL = "https://h5api.m.goofish.com/h5"
    APP_KEY = "34839810"
    PASSPORT_HOST = "https://passport.goofish.com"
    
    def __init__(self, state_manager, browser_manager=None):
        """
        Args:
            state_manager: 状态管理器
            browser_manager: 浏览器管理器（用于重新登录）
        """
        self.state = state_manager
        self.browser = browser_manager
        self.session = None
        self._request_lock = asyncio.Lock()
        self._browser_refresh_attempted = False  # 防止浏览器刷新循环
        self._token_refresh_count = 0  # Token刷新计数（防止死循环）
        self._max_token_refresh = 3  # 最大刷新次数
    
    def get_token(self) -> str:
        """从cookie中获取token"""
        cookies = self.state.get("cookie", {})
        if not cookies:
            logger.error('从state获取token时cookie为空')
            return ""
        if '_m_h5_tk' not in cookies:
            return ""
        token_value = cookies['_m_h5_tk']
        # ✅ 检查token是否为空字符串（服务器可能设置了空的token来删除）
        if not token_value or token_value.strip() == "":
            return ""
        return token_value.split('_')[0]
    
    def get_timestamp(self) -> int:
        """获取时间戳（毫秒）"""
        return int(time.time() * 1000)
    
    def build_sign(self, timestamp: int, data: str) -> str:
        """
        构建签名
        
        ✅ 关键修复：根据HAR文件分析，签名计算规则：
        1. 有token时：只使用hash部分（`_`之前的部分），不包含时间戳
        2. 无token时：使用字符串"undefined"，而不是空字符串
        
        Args:
            timestamp: 时间戳（毫秒）
            data: JSON数据（字符串）
        Returns:
            MD5签名
        """
        token = self.get_token()
        # ✅ 根据HAR文件分析：当没有token时，应该使用"undefined"而不是空字符串
        # 注意：get_token()已经正确地只返回hash部分（split('_')[0]）
        if not token or token.strip() == "":
            token = "undefined"
            logger.debug("⚠️ Token为空，使用'undefined'构建签名")
        sign_str = f"{token}&{timestamp}&{self.APP_KEY}&{data}"
        sign = hashlib.md5(sign_str.encode()).hexdigest()
        return sign
    
    def build_url(self, api: APIDefinition, timestamp: int, sign: str, pass_risk: bool = False) -> str:
        """
        构建请求URL
        Args:
            api: API定义
            timestamp: 时间戳
            sign: 签名
            pass_risk: 是否通过风控
        """
        url = f"{self.HOST_URL}/{api.api_name}/{api.version}/?"
        params = [
            "jsv=2.7.2",
            f"appKey={self.APP_KEY}",
            f"t={timestamp}",
            f"dataType={api.data_type}",
            f"type={api.format_type}",
            f"api={api.api_name}",
            "timeout=20000",
            "sessionOption=AutoLoginOnly",
            "accountSite=xianyu",
            f"sign={sign}",
            f"v={api.version}",
            f"spm_cnt={api.spm_cnt}"
        ]
        
        if api.spm_pre:
            params.append(f"spm_pre={api.spm_pre}")
            log_id = api.spm_pre.split('.')[-1]
            if log_id != "0":
                params.append(f"log_id={log_id}")
        
        # 风控通过参数
        if pass_risk:
            params.append("dangerouslySetWindvaneParams=%5Bobject%20Object%5D")
            params.append("smToken=token")
            params.append("queryToken=sm")
            params.append("sm=sm")

        # 关键发现：删除token后直接请求，服务器会自动返回新token
        current_token = self.get_token()
        if not current_token or current_token == "undefined" or current_token == "":
            # 可选：如果需要，可以添加（根据测试结果决定）
            # params.append("dangerouslySetWindvaneParams=%5Bobject%20Object%5D")
            logger.debug("⚠️ Token为空，服务器会自动返回新token")
        
        # ✅ 订单相关API需要添加valueType=string参数（参考C#代码：OrderApiRequest.buildUrl）
        if api == GoofishAPIs.CREATE_ORDER or api == GoofishAPIs.ORDER_RENDER:
            params.append("valueType=string")
        
        url += "&".join(params)
        return url
    
    def build_headers(self, referer: str = "https://www.goofish.com") -> Dict[str, str]:
        """构建请求头"""
        return {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
            "sec-ch-ua": '"Not?A_Brand";v="8", "Chromium";v="138"',
            "Accept": "application/json",
            "accept-language": "zh-CN,zh;q=0.9",
            "Origin": "https://www.goofish.com",
            "referer": referer,
            "dnt": "1",
            "Sec-Fetch-Site": "same-site",
            "Sec-Fetch-Mode": "cors",
            "Sec-Ch-Ua-Mobile": "?0",
            "Sec-Fetch-Dest": "empty",
            "Sec-Ch-Ua-Platform": '"Windows"',
            "priority": "u=1, i"
        }
    
    async def get_session(self) -> aiohttp.ClientSession:
        """
        获取或创建session，并同步浏览器cookie到session
        
        ✅ 重要：x5sec cookie需要被保护，不会被浏览器cookie覆盖
        - 如果state中有x5sec，优先保留state中的x5sec
        - 浏览器同步过来的cookie不会覆盖已存在的x5sec
        """
        # ✅ 保存当前的x5sec（如果有），防止被浏览器cookie覆盖
        current_cookies = self.state.get("cookie", {})
        preserved_x5sec = current_cookies.get("x5sec")
        
        # ✅ 优化：不每次都从浏览器同步cookie，只在以下情况同步：
        #    1. state中没有cookie（首次启动）
        #    2. 浏览器可用且cookie可能已更新（但不强制，避免浏览器死掉时阻塞）
        # ✅ 关键：API搜索的意义就是不需要浏览器，应该优先使用已有cookie
        if self.browser and not current_cookies:
            # 只有在state中没有cookie时才从浏览器获取（但不强制浏览器可用）
            try:
                # ✅ 使用auto_restart=False，避免浏览器死掉时重启导致请求变慢
                latest_cookies = await self.browser.get_cookie_dict(auto_restart=False)
                if latest_cookies:
                    # ✅ 如果state中有x5sec，保留它（浏览器cookie不会覆盖x5sec）
                    if preserved_x5sec:
                        latest_cookies['x5sec'] = preserved_x5sec
                    
                    # 更新state中的cookie
                    self.state.update(cookie=latest_cookies)
                    # logger.debug("✅ 已从浏览器同步cookie到state（state中无cookie）")
            except Exception as e:
                # 同步失败不影响，可能浏览器僵死了或者关闭了，使用已有cookie继续
                logger.debug(f"从浏览器同步cookie失败（不影响使用）: {e}")
        
        if not self.session or self.session.closed:
            # 创建cookie jar并添加当前cookie
            jar = aiohttp.CookieJar()
            cookies = self.state.get("cookie", {})
            if cookies:
                for name, value in cookies.items():
                    jar.update_cookies({name: value})
            
            self.session = aiohttp.ClientSession(cookie_jar=jar)
        else:
            # ✅ Session已存在时，也更新cookie jar（浏览器可能已更新cookie）
            cookies = self.state.get("cookie", {})
            if cookies:
                # 更新现有session的cookie jar
                for name, value in cookies.items():
                    self.session.cookie_jar.update_cookies({name: value})
        
        return self.session
    
    async def close(self):
        """关闭session（带超时保护）"""
        if self.session and not self.session.closed:
            try:
                # ✅ 使用超时保护，避免关闭时卡住
                await asyncio.wait_for(self.session.close(), timeout=2.0)
            except asyncio.TimeoutError:
                logger.warning("关闭ClientSession超时，强制关闭")
                # 超时后强制关闭
                if not self.session.closed:
                    self.session._connector = None
            except Exception as e:
                logger.warning(f"关闭ClientSession时出错: {e}")
            finally:
                self.session = None
    
    async def request(
        self,
        api: APIDefinition,
        data: Dict[str, Any] = None,
        is_retry: bool = False,
        pass_risk: bool = False,
        **kwargs
    ) -> Dict[str, Any]:
        """
        发送API请求
        
        ✅ 重要：在请求开始时检查任务状态，如果任务已停止则立即返回错误
        Args:
            api: API定义
            data: 请求数据
            is_retry: 是否为重试请求
            pass_risk: 是否通过风控
        Returns:
            API响应
        """
        # ✅ 检查任务是否已停止
        # 注意：当任务停止时，work_status会被设置为"stopped"或其他非正常状态
        # 但如果是idle，可能是正常状态（未开始任务），也可能是任务已停止
        # 因此需要额外检查browser_status，如果browser_status是stopped，说明任务已停止
        work_status = self.state.get("work_status")
        browser_status = self.state.get("browser_status")
        
        # ✅ 如果浏览器状态是stopped，尝试自动启动浏览器（如果是必要的操作）
        # ✅ 但如果是搜索/下单操作，确实需要浏览器，应该先启动
        force_check_login = kwargs.get("force_check_login", False)
        
        if browser_status == "stopped":
            # ✅ 对于某些操作（如获取地址、获取用户ID等），如果浏览器未运行，尝试自动启动
            # ✅ 但如果浏览器启动失败或正在关闭，则拒绝请求
            is_critical_operation = force_check_login or api.api_name in [
                "mtop.taobao.idle.logistic.address.list.query",
                "mtop.taobao.idlemessage.pc.loginuser.get"
            ]
            
            if is_critical_operation and self.browser:
                logger.info(f"⚠️  浏览器已停止，但操作需要浏览器，尝试自动启动: {api.api_name}")
                try:
                    # ✅ 快速启动浏览器（不等待登录验证）
                    if not self.browser.browser or not self.browser.page:
                        await self.browser.start_browser()
                        logger.info("✅ 浏览器已自动启动")
                    else:
                        # 浏览器对象存在但状态是stopped，更新状态
                        self.browser.state.update(browser_status="running")
                        logger.debug("✅ 浏览器状态已更新为running")
                except Exception as e:
                    logger.warning(f"⚠️  自动启动浏览器失败: {e}，取消API请求")
                    return {
                        "ret": [ResponseCode.RGV587_ERROR],
                        "data": {},
                        "msg": f"浏览器已停止且启动失败: {e}",
                        "error_type": "task_stopped"
                    }
            else:
                # ✅ 非关键操作或浏览器不可用，直接拒绝
                logger.warning(f"⚠️  浏览器已停止（browser_status=stopped），取消API请求: {api.api_name}")
                return {
                    "ret": [ResponseCode.RGV587_ERROR],
                    "data": {},
                    "msg": "浏览器已停止，取消API请求",
                    "error_type": "task_stopped"
                }
        
        # ✅ 检查封禁状态：封禁状态只阻止搜索和下单相关操作
        # ✅ 封禁是对搜索功能的封禁，但不影响其他操作（如获取地址、取消订单等）
        # ✅ 如果force_check_login=True（用于登录检查），允许调用API来检查真实状态
        if self.state.is_banned() and not force_check_login:
            # ✅ 判断是否为搜索或下单相关API
            is_search_or_order_api = api in [GoofishAPIs.SEARCH, GoofishAPIs.ORDER_RENDER, GoofishAPIs.CREATE_ORDER]
            
            if is_search_or_order_api:
                # ✅ 搜索和下单API被阻止
                ban_msg = self.state.get_ban_message()
                logger.warning(f"⚠️  账号被封禁，取消API请求: {api.api_name}, {ban_msg}")
                return {
                    "ret": [ResponseCode.RGV587_ERROR],
                    "data": {},
                    "msg": f"账号被封禁，无法执行操作: {ban_msg}",
                    "error_type": "account_banned",
                    "banned": True
                }
            else:
                # ✅ 其他API（如获取地址、取消订单等）不受封禁限制
                logger.debug(f"🔍 账号被封禁，但允许调用非搜索/下单API: {api.api_name}")
        elif self.state.is_banned() and force_check_login:
            # ✅ 登录检查时，即使标记为封禁，也允许调用API检查真实状态
            logger.info(f"🔍 登录检查模式：即使标记为封禁，也调用API检查真实的登录状态: {api.api_name}")
        
        # ✅ 检查非法请求错误：非法请求（FAIL_SYS_ILLEGAL_ACCESS）表示已被风控检测到，必须立即停止所有操作
        # ✅ 非法请求一次就不能继续，更不能有阈值，因为已经被风控检测
        if work_status == "error":
            last_error = self.state.get("last_error", "")
            # ✅ 检查是否是非法请求错误
            if "[ILLEGAL_ACCESS]" in last_error or "illegal_access" in last_error.lower() or "API请求异常" in last_error:
                logger.warning(f"检测到非法请求错误，取消API请求: {api.api_name}")
                return {
                    "ret": [ResponseCode.RGV587_ERROR],
                    "data": {},
                    "msg": f"检测到非法请求错误，已被风控检测，无法继续操作: {last_error}",
                    "error_type": "illegal_access",
                    "illegal_access": True
                }
        
        # ✅ 重要：重试请求不使用锁，因为重试是在第一次请求的上下文中进行的（已在锁内）
        # 如果重试请求也获取锁，会导致死锁（重试请求等待锁，但锁被第一次请求持有）
        if is_retry:
            # 重试请求直接执行，不使用锁
            logger.debug(f"🔄 重试请求跳过锁检查（已在第一次请求的锁内）")
        else:
            # 第一次请求需要获取锁，避免并发冲突
            async with self._request_lock:
                return await self._execute_request(api, data, is_retry, pass_risk, False)
        
        # ✅ 重试请求的执行（不使用锁）
        return await self._execute_request(api, data, is_retry, pass_risk, False)
    
    async def _execute_request(
        self,
        api: APIDefinition,
        data: Dict[str, Any] = None,
        is_retry: bool = False,
        pass_risk: bool = False,
        force_test: bool = False
    ) -> Dict[str, Any]:
        # ✅ 检查是否在冷却期内（在请求前检查，避免无效请求）
        import time
        cooldown_until = self.state.get("risk_cooldown_until", 0)
        if cooldown_until and time.time() < cooldown_until:
            remaining_minutes = int((cooldown_until - time.time()) / 60)
            remaining_seconds = int((cooldown_until - time.time()) % 60)
            logger.warning(f"⚠️ 账号仍在冷却期内，剩余 {remaining_minutes}分{remaining_seconds}秒，跳过本次请求")
            return {
                "ret": [ResponseCode.RGV587_ERROR],
                "data": {},
                "msg": f"账号冷却中，剩余 {remaining_minutes}分{remaining_seconds}秒",
                "error_type": "risk_control_cooldown",
                "cooldown_until": cooldown_until,
                "remaining_minutes": remaining_minutes
            }
        """
        执行实际的API请求（不包含锁，由调用方控制锁）
        """
        # ✅ 保存force_test到实例变量，供_handle_response使用（用于重试）
        self._current_force_test = force_test
        
        # ✅ 关键修复：直接发送请求，让服务器返回错误码（如FAIL_SYS_TOKEN_EMPTY）
        # 原因：ApiPost使用空token能正常返回FAIL_SYS_TOKEN_EMPTY，说明token为空不会导致"非法请求"
        # 问题在于：如果预请求刷新token时签名计算错误，反而会导致"非法请求"
        # 解决方案：直接发送请求，在_handle_response中根据服务器返回的错误码决定是否刷新token
        
        request_data = json.dumps(data or {}, ensure_ascii=False, separators=(',', ':'))
        timestamp = self.get_timestamp()
        
        # ✅ 调试：记录请求使用的token
        current_token = self.get_token()
        if is_retry:
            logger.info(f"🔄 重试请求构建签名: token={current_token[:30] if current_token and current_token != 'undefined' else 'undefined'}..., timestamp={timestamp}")
        elif not current_token:
            logger.debug("⚠️ Token为空，直接发送请求（服务器会返回FAIL_SYS_TOKEN_EMPTY，然后根据错误码刷新token）")
        
        sign = self.build_sign(timestamp, request_data)
        url = self.build_url(api, timestamp, sign, pass_risk)
        headers = self.build_headers()
        
        # 特殊处理搜索API的referer（包含搜索关键词）
        if api == GoofishAPIs.SEARCH:
            keyword = data.get("keyword", "") if isinstance(data, dict) else ""
            if keyword:
                # URL编码搜索关键词
                encoded_keyword = quote(keyword)
                headers["referer"] = f"https://www.goofish.com/search?q={encoded_keyword}&spm=a21ybx.home.searchInput.0"
            else:
                headers["referer"] = "https://www.goofish.com/search?spm=a21ybx.home.searchInput.0"
        
        # 特殊处理地址API的referer
        if api == GoofishAPIs.ADDRESS_LIST:
            headers["referer"] = "https://www.goofish.com/create-order?spm=a21ybx.item.buy.1.28e03da6CpdHIg&itemId=921527494136"
        
        # 特殊处理取消订单API的referer（参考浏览器请求）
        if api == GoofishAPIs.CLOSE_ORDER:
            headers["referer"] = "https://www.goofish.com/bought?spm=a21ybx.create-order.nav.2"
        
        # ✅ 保存完整的请求信息用于诊断（URL、Headers、Cookie、POST data）
        cookies = self.state.get("cookie", {})
        request_info = {
            "api_name": api.api_name,
            "api_version": api.version,
            "url": url,
            "method": "POST",
            "headers": dict(headers),  # 转换为普通字典
            "cookies": dict(cookies),  # 保存cookie副本（用于诊断）
            "post_data": request_data,  # JSON格式的原始数据
            "post_data_encoded": f"data={quote(request_data)}",  # URL编码后的POST body
            "timestamp": timestamp,
            "sign": sign,
            "app_key": self.APP_KEY
        }
        
        try:
            # ✅ 再次检查任务状态（可能在获取session之前任务被停止）
            # 注意：对于重试请求（is_retry=True），允许更宽松的检查，因为可能是风控验证成功后的重试
            work_status = self.state.get("work_status")
            browser_status = self.state.get("browser_status")
            
            # ✅ 如果是重试请求（风控验证成功后），允许在browser_status=stopped时继续（浏览器可能正在关闭中）
            if not is_retry:
                # ✅ 如果浏览器状态是stopped，说明任务已停止，取消API请求
                if browser_status == "stopped":
                    logger.warning(f"⚠️  浏览器已停止（browser_status=stopped），取消API请求: {api.api_name}")
                    return {
                        "ret": [ResponseCode.RGV587_ERROR],
                        "data": {},
                        "msg": "浏览器已停止，取消API请求",
                        "error_type": "task_stopped"
                    }
                
                # ✅ 如果work_status不是searching/ordering/idle，说明任务异常或已停止
                # ✅ 但是，如果force_test=True（用于测试封禁范围），允许调用API来测试真实状态
                # ✅ 注意：封禁时，只有搜索和下单API会被阻止，其他API不受限制
                if work_status not in ["searching", "ordering", "idle"]:
                    if force_test:
                        # ✅ 测试模式下，即使work_status不是idle/searching/ordering，也允许调用API
                        logger.info(f"🔍 测试模式：即使work_status={work_status}，也调用API测试真实状态: {api.api_name}")
                    else:
                        # ✅ 判断是否为搜索或下单相关API
                        is_search_or_order_api = api in [GoofishAPIs.SEARCH, GoofishAPIs.ORDER_RENDER, GoofishAPIs.CREATE_ORDER]
                        if is_search_or_order_api:
                            logger.warning(f"⚠️  任务已停止（work_status={work_status}），取消API请求: {api.api_name}")
                            return {
                                "ret": [ResponseCode.RGV587_ERROR],
                                "data": {},
                                "msg": f"任务已停止（work_status={work_status}），取消API请求",
                                "error_type": "task_stopped"
                            }
                        else:
                            # ✅ 其他API（如获取地址、取消订单等）不受work_status限制
                            logger.debug(f"🔍 任务状态异常（work_status={work_status}），但允许调用非搜索/下单API: {api.api_name}")
            
            session = await self.get_session()
            
            # ✅ 关键修复：手动构建Cookie请求头（像ApiPost一样）
            # 原因：aiohttp的cookie_jar可能不会自动添加所有cookie到请求头（需要domain和path匹配）
            # ApiPost中是在Header中固定设置Cookie，所以更可靠
            # 关键发现：即使ApiPost中删除所有cookie，也能正常返回错误码（不是"非法操作"）
            # 说明：服务器判断"非法请求"的依据不是Cookie是否存在，而是其他因素（签名、请求格式等）
            cookie_header_parts = []
            
            # ✅ 始终构建Cookie头（即使cookies为空）
            # 如果cookies存在，遍历所有cookie
            if cookies:
                for name, value in cookies.items():
                    # ✅ 确保cookie值不为None（使用空字符串代替）
                    cookie_value = value if value is not None else ""
                    # ✅ 即使值为空，也要添加到Cookie头（这样服务器才能识别token为空）
                    cookie_header_parts.append(f"{name}={cookie_value}")
            
            # ✅ 特别处理：确保_m_h5_tk始终存在（即使为空）
            # 这样服务器才能返回FAIL_SYS_TOKEN_EMPTY而不是FAIL_SYS_ILLEGAL_ACCESS
            has_token = False
            if cookies and '_m_h5_tk' in cookies:
                # 如果cookies中有_m_h5_tk（即使值为空），已经在上面添加过了
                has_token = True
            
            if not has_token:
                # 如果没有_m_h5_tk，检查是否已经在cookie_header_parts中
                has_token_header = any(part.startswith('_m_h5_tk=') for part in cookie_header_parts)
                if not has_token_header:
                    # 添加空的_m_h5_tk，让服务器识别为"令牌为空"
                    cookie_header_parts.append("_m_h5_tk=")
            
            # ✅ 始终设置Cookie请求头（即使为空，也要设置，模拟真实浏览器行为）
            if cookie_header_parts:
                cookie_header_str = "; ".join(cookie_header_parts)
                headers["Cookie"] = cookie_header_str
                logger.debug(f"✅ 已手动设置Cookie请求头: {len(cookie_header_parts)}个cookie")
            else:
                # 如果完全没有cookie，设置一个空的Cookie头（模拟真实浏览器）
                headers["Cookie"] = ""
                logger.debug("⚠️ Cookie为空，设置空Cookie请求头")
            
            # 发送POST请求
            post_data = f"data={quote(request_data)}"
            headers["Content-Type"] = "application/x-www-form-urlencoded"
            
            # ✅ 添加详细日志：打印请求信息（用于诊断FAIL_SYS_ILLEGAL_ACCESS问题）
            # if not is_retry:  # 只在首次请求时打印，避免日志过多
            #     logger.debug(f"📤 发送API请求详情:")
            #     logger.debug(f"   URL: {url[:100]}...")
            #     logger.debug(f"   Headers: {json.dumps({k: v for k, v in headers.items() if k != 'Cookie'}, ensure_ascii=False)}")
            #     logger.debug(f"   Cookie: {headers.get('Cookie', '(无)')[:200]}...")
            #     logger.debug(f"   Data: {request_data[:200]}...")
            
            async with session.post(url, data=post_data, headers=headers, timeout=20) as response:
                response_text = await response.text()
                result = json.loads(response_text)
                
                # ✅ 添加日志，便于诊断重试请求的响应
                # if is_retry:
                #     logger.info(f"📥 重试请求响应 (api={api.api_name}): code={result.get('ret', ['unknown'])[0] if result.get('ret') else 'no_code'}")
                #     logger.debug(f"重试请求完整响应 (api={api.api_name}): {json.dumps(result, ensure_ascii=False)[:500]}")
                
                # 第一次请求时处理各种异常
                if not is_retry:
                    return await self._handle_response(api, data, result, pass_risk, is_retry, request_info)
                
                # ✅ 重试请求：也需要检查响应，因为可能返回成功或新的错误
                # 注意：重试请求可能来自风控验证成功后，此时应该返回成功响应
                ret_codes = result.get("ret", [])
                if ret_codes:
                    code = ret_codes[0]
                    # 如果重试请求成功，直接返回结果
                    if code == ResponseCode.SUCCESS or (isinstance(code, str) and code.startswith("SUCCESS")):
                        logger.success(f"✅ 重试请求成功 (api={api.api_name})")
                        return result
                    # 如果重试请求仍然失败，记录日志但直接返回（不再递归处理，避免无限循环）
                    else:
                        logger.warning(f"⚠️  重试请求仍然失败 (api={api.api_name}, code={code}, msg={result.get('msg', 'N/A')})")
                        return result
                else:
                    logger.error(f"❌ 重试请求响应格式错误 (api={api.api_name}): {result}")
                    return result
                    
        except asyncio.TimeoutError:
            logger.error(f"API请求超时: {api.api_name}, is_retry={is_retry}")
            return {"ret": ["FAIL_SYS_REQUEST_TIMEOUT"], "data": {}, "msg": "请求超时"}
        except Exception as e:
            logger.exception(f"API请求异常: {api.api_name}, is_retry={is_retry}, error: {e}")
            return {"ret": ["FAIL_SYS_REQUEST_ERROR"], "data": {}, "msg": str(e)}
    
    async def _handle_response(
        self,
        api: APIDefinition,
        data: Dict[str, Any],
        result: Dict[str, Any],
        pass_risk: bool,
        is_retry: bool = False,
        request_info: Dict[str, Any] = None
    ) -> Dict[str, Any]:
        """
        处理API响应，包含token过期、登录过期、风控等逻辑
        Args:
            api: API定义
            data: 请求数据
            result: API响应
            pass_risk: 是否已通过风控
        Returns:
            处理后的响应
        """
        ret_codes = result.get("ret", [])
        if not ret_codes:
            return result
        
        code = ret_codes[0] if ret_codes else ""
        # ✅ 错误码可能是完整字符串，如 "RGV587_ERROR::SM::..."，需要字符串包含检查
        code_str = str(code)
        
        # 1. Token为空、过期或非法
        # ⚠️ FAIL_SYS_TOKEN_ILLEGAL：token被修改或无效（如手动修改_m_h5_tk）
        if code in [ResponseCode.FAIL_SYS_TOKEN_EMPTY, ResponseCode.FAIL_SYS_TOKEN_EXPIRED, ResponseCode.FAIL_SYS_TOKEN_ILLEGAL] or \
           ResponseCode.FAIL_SYS_TOKEN_EXPIRED in code_str or "FAIL_SYS_TOKEN_EXOIRED" in code_str or \
           "FAIL_SYS_TOKEN_ILLEGAL" in code_str or "TOKEN_ILLEGAL" in code_str or "非法令牌" in code_str:
            if "ILLEGAL" in code_str or "非法令牌" in code_str:
                logger.warning(f"⚠️ Token非法（可能被修改）: {code}，刷新token")
            else:
                logger.debug(f"Token过期或为空: {code}，刷新token")
            
            # ⚠️ 防止死循环：检查刷新次数
            if self._token_refresh_count >= self._max_token_refresh:
                logger.error(f"Token刷新次数已达上限({self._max_token_refresh})，Session已过期")
                return {
                    "ret": [ResponseCode.FAIL_SYS_SESSION_EXPIRED],
                    "data": {},
                    "msg": "Token刷新失败，Session已过期，请使用浏览器刷新页面重新登录",
                    "error_type": "session_expired"
                }
            
            self._token_refresh_count += 1
            token_result = await self._refresh_token()
            
            if not token_result.get("success"):
                logger.error(f"Token刷新失败 (第{self._token_refresh_count}次)")
                # ✅ 如果token刷新失败且标记为需要浏览器刷新（session_expired=True），则刷新浏览器
                # 注意：此时已经尝试过刷新token了，如果还是失败，说明需要浏览器刷新获取新cookie
                # session_expired=True 表示需要浏览器刷新，而不是再次刷新token
                if token_result.get("session_expired"):
                    self._token_refresh_count = 0  # 重置计数
                    logger.warning("Token刷新失败：需要浏览器刷新获取新cookie...")
                    
                    # ✅ 只有在token刷新失败且需要浏览器刷新时才刷新浏览器
                    if self.browser and not is_retry and not self._browser_refresh_attempted:
                        self._browser_refresh_attempted = True  # 防止循环
                        try:
                            logger.info("🔄 Token刷新失败，使用浏览器刷新页面获取新cookie...")
                            refresh_success = await self.browser.refresh_and_check_login()
                            if refresh_success:
                                logger.success("✅ 浏览器页面刷新成功，重试原API请求")
                                self._browser_refresh_attempted = False
                                # 重试原API请求
                                return await self.request(api, data, is_retry=True)
                            else:
                                logger.warning("⚠️ 浏览器页面刷新失败")
                        except Exception as e:
                            logger.error(f"浏览器页面刷新异常: {e}")
                        finally:
                            self._browser_refresh_attempted = False
                    
                    # 标记登录失败
                    self.state.mark_logout()
                    self.state.set_error("登录已过期，请重新登录")
                    
                    return {
                        "ret": [ResponseCode.FAIL_SYS_SESSION_EXPIRED],
                        "data": {},
                        "msg": "Token刷新失败：Session已过期，浏览器刷新也失败，请手动重新登录",
                        "error_type": "session_expired"
                    }
                self._token_refresh_count = 0  # 重置计数
                return {
                    "ret": [code],
                    "data": {},
                    "msg": "Token刷新失败",
                    "error_type": "token_error"
                }
            
            # ✅ Token刷新成功，重置计数
            self._token_refresh_count = 0
            
            # ✅ Token刷新成功，检查登录状态
            logger.debug("Token刷新成功，检查登录状态...")
            is_logged = False
            if self.browser:
                try:
                    is_logged = await self.browser.check_login()
                except Exception as e:
                    logger.debug(f"检查登录状态失败: {e}")
            else:
                login_status = self.state.get("login_status", "not_logged_in")
                is_logged = login_status == "logged_in"
            
            if not is_logged:
                logger.error("账号未登录，无法执行操作")
                return {
                    "ret": [ResponseCode.FAIL_SYS_SESSION_EXPIRED],
                    "data": {},
                    "msg": "账号未登录，请先登录",
                    "error_type": "session_expired"
                }
            
            # ✅ 已登录，更新session并重试请求
            logger.debug("登录状态正常，重试请求")
            try:
                session = await self.get_session()
                cookies = self.state.get("cookie", {})
                if cookies:
                    # ✅ 关键修复：更新所有关键cookie，不仅仅是_m_h5_tk
                    # 原因：_m_h5_tk和_m_h5_tk_enc必须匹配，否则会导致FAIL_SYS_ILLEGAL_ACCESS
                    key_cookies = ['_m_h5_tk', '_m_h5_tk_enc', 'cookie2', 'mtop_partitioned_detect']
                    cookies_to_update = {k: v for k, v in cookies.items() if k in key_cookies and v}
                    if cookies_to_update:
                        session.cookie_jar.update_cookies(cookies_to_update)
                        logger.debug(f"✅ 已更新关键cookie到session: {list(cookies_to_update.keys())}")
            except Exception as e:
                logger.debug(f"更新session cookie失败: {e}")
            
            # ✅ 最多重试一次
            if not is_retry:
                return await self.request(api, data, is_retry=True)
            else:
                logger.error("Token刷新后重试仍然失败")
                return {
                    "ret": [code],
                    "data": {},
                    "msg": f"Token刷新后重试失败: {result.get('msg', 'unknown')}",
                    "error_type": "token_error"
                }
        
        # 1.5 非法请求（API请求异常：签名不对、参数有误、token和加密token不匹配等）
        # ❌ 非法请求坚决不能出现，出现就说明流程有问题或cookie操作有bug
        # ✅ 注意：RGV587_ERROR 是人机检测风控（滑块、九宫格），由第3部分处理
        # ✅ FAIL_SYS_ILLEGAL_ACCESS 是明确的请求异常，说明token签名计算错误或cookie不匹配
        elif "FAIL_SYS_ILLEGAL_ACCESS" in str(code) or "ILLEGAL_ACCESS" in str(code):
            error_msg = result.get("msg", "API请求异常")
            
            logger.error("=" * 70)
            logger.error(f"❌ 检测到非法请求: {code} - {error_msg}")
            logger.error("⚠️  非法请求说明流程有问题：")
            logger.error("   1. Token签名计算错误（token、timestamp、appKey、data）")
            logger.error("   2. Cookie不匹配（_m_h5_tk 和 _m_h5_tk_enc 不匹配）")
            logger.error("   3. 请求参数格式错误")
            logger.error("=" * 70)
            
            # ✅ 记录详细的诊断信息（帮助排查问题）
            current_token = self.get_token()
            current_token_enc = self.state.get("cookie", {}).get("_m_h5_tk_enc", "")
            current_timestamp = self.get_timestamp()
            logger.error(f"诊断信息：")
            logger.error(f"  当前token: {current_token[:30] if current_token else '(空)'}...")
            logger.error(f"  当前_m_h5_tk_enc: {current_token_enc[:30] if current_token_enc else '(空)'}...")
            logger.error(f"  token和加密token是否都存在: {bool(current_token and current_token_enc)}")
            logger.error(f"  API: {api.api_name}")
            logger.error(f"  时间戳: {current_timestamp}")
            
            # ✅ 验证token和加密token是否匹配（这是导致非法请求的常见原因）
            if current_token and current_token_enc:
                logger.error(f"  ✅ Token和加密Token都存在，但不匹配导致非法请求")
            elif current_token and not current_token_enc:
                logger.error(f"  ❌ 有token但缺少_m_h5_tk_enc，会导致非法请求")
            elif not current_token and current_token_enc:
                logger.error(f"  ❌ 有_m_h5_tk_enc但缺少token，会导致非法请求")
            else:
                logger.error(f"  ❌ Token和加密Token都不存在，会导致非法请求")
            
            logger.error(f"请求数据: {json.dumps(data, ensure_ascii=False, indent=2) if data else '{}'}")
            
            # ❌ 非法请求不能继续，必须立即停止并标记错误
            # ❌ 不能尝试刷新token重试，因为这说明流程本身有问题（token签名计算错误或cookie不匹配）
            logger.error("❌ 非法请求必须立即停止，不能继续操作")
            self.state.set_error(f"[ILLEGAL_ACCESS] API请求异常: {error_msg} - 流程有问题（token签名或cookie不匹配），必须停止")
            
            # ✅ 发送异常消息到控制端（实时日志），包含完整的请求诊断信息
            if self.browser and hasattr(self.browser, 'tcp_client') and self.browser.tcp_client:
                try:
                    from core.command_manager import CommandRes
                    # ✅ 获取账号信息用于日志显示
                    account = self.state.get("account") or ""
                    account_nickname = self.state.get("account_nickname") or ""
                    client_name = getattr(self.browser, 'client_name', '') or ""
                    
                    # ✅ 构建账号显示信息（优先使用昵称，其次账号名，最后客户端名）
                    account_display = account_nickname or account or client_name or ""
                    
                    # ✅ 包含完整的请求诊断信息（URL、Headers、Cookie、POST data）
                    error_log_data = {
                        "api": api.api_name,
                        "error_code": code,
                        "error_msg": error_msg,
                        "request_data": data,
                        "response": result,
                        "account": account_display,  # ✅ 添加账号信息
                        "client_id": self.state.get("client_id", "")
                    }
                    # 如果有请求信息，添加到日志数据中
                    if request_info:
                        error_log_data["request_details"] = request_info
                    
                    error_log_msg = {
                        "event": "api_error",
                        "direction": "res",
                        "success": False,
                        "code": 1,
                        "msg": f"API请求异常: {error_msg}",
                        "data": error_log_data
                    }
                    
                    # 通过TCP发送错误日志（控制端会转发到WebSocket实时日志）
                    asyncio.create_task(self.browser.tcp_client.send(error_log_msg))
                    
                    # 同时发送状态更新（标记主机异常，work_status=error）
                    asyncio.create_task(self.browser.tcp_client.send_status(include_cookie=False))
                    logger.info("✅ 已发送API异常日志到控制端实时日志，并标记主机异常（work_status=error）")
                except Exception as e:
                    logger.warning(f"发送异常日志失败: {e}")
            
            # ✅ 暂停继续执行API避免封禁（不再重试）
            # ✅ 返回完整的请求诊断信息，便于前端和日志查看
            error_response = {
                "ret": [code],
                "data": {},
                "msg": f"API请求异常: {error_msg}",
                "error_type": "illegal_access",
                "request_data": data,  # 包含请求内容便于前端调试
                "response": result  # 包含响应内容便于前端调试
            }
            # 如果有请求信息，添加到响应中
            if request_info:
                error_response["request_details"] = request_info
            return error_response
        
        # 2. 登录过期 - ✅ 依赖API返回的code判断，不依赖state或本地判断
        elif code == ResponseCode.FAIL_SYS_SESSION_EXPIRED:
            # ✅ 根据HAR文件分析：返回FAIL_SYS_SESSION_EXPIRED时，浏览器会删除token，然后重新请求获取新token
            # 流程：1. 删除_m_h5_tk和_m_h5_tk_enc 2. 重新请求（不带token或带空token）3. 服务器通过Set-Cookie返回新token
            logger.warning(f"API返回Session过期: {code}，删除token并重新请求获取新token")
            
            # ✅ 删除token（模拟浏览器行为）
            cookies = self.state.get("cookie", {})
            if '_m_h5_tk' in cookies:
                logger.info("🗑️  删除旧的_m_h5_tk")
                del cookies['_m_h5_tk']
            if '_m_h5_tk_enc' in cookies:
                logger.info("🗑️  删除旧的_m_h5_tk_enc")
                del cookies['_m_h5_tk_enc']
            self.state.set("cookie", cookies)
            
            # ✅ 重新请求（不带token，让服务器返回新token）
            if not is_retry:
                logger.info("🔄 重新请求获取新token（不带token）...")
                # 清除token缓存，强制重新获取
                self._token_cache = None
                result = await self.request(api, data, is_retry=True)
                
                # ✅ 从session的cookie_jar中提取新的token（服务器通过Set-Cookie返回）
                try:
                    session = await self.get_session()
                    new_cookies = {}
                    for cookie in session.cookie_jar:
                        new_cookies[cookie.key] = cookie.value
                    
                    # 检查是否获取到新的token
                    if '_m_h5_tk' in new_cookies and new_cookies['_m_h5_tk']:
                        logger.info(f"✅ 从响应中获取到新token: {new_cookies['_m_h5_tk'][:30]}...")
                        # 更新state中的cookie
                        current_cookies = self.state.get("cookie", {})
                        current_cookies.update(new_cookies)
                        self.state.set("cookie", current_cookies)
                        logger.info("✅ 已更新state中的cookie（包含新token）")
                except Exception as e:
                    logger.debug(f"从响应中提取cookie失败（不影响使用）: {e}")
                
                return result
            else:
                # 如果重试后仍然失败，尝试刷新浏览器
                logger.warning("⚠️ 重试后仍然返回Session过期，尝试刷新浏览器")
                if self.browser and not self._browser_refresh_attempted:
                    self._browser_refresh_attempted = True  # 防止循环
                    try:
                        logger.info("🔄 重试失败，使用浏览器刷新页面恢复登录...")
                        refresh_success = await self.browser.refresh_and_check_login()
                        if refresh_success:
                            logger.success("✅ 浏览器页面刷新成功，再次重试原API请求")
                            self._browser_refresh_attempted = False
                            # 再次重试原API请求
                            return await self.request(api, data, is_retry=False)
                        else:
                            logger.warning("⚠️ 浏览器页面刷新失败")
                    except Exception as e:
                        logger.error(f"浏览器页面刷新异常: {e}")
                    finally:
                        self._browser_refresh_attempted = False
                
                # 标记登录失败
                self.state.mark_logout()
                self.state.set_error("登录已过期，请重新登录")
                
                # ✅ 主动发送状态更新到控制端（通知前端登录状态已变化）
                if self.browser and hasattr(self.browser, 'tcp_client') and self.browser.tcp_client:
                    try:
                        asyncio.create_task(self.browser.tcp_client.send_status(include_cookie=False))
                        logger.info("已通知控制端：登录状态已过期")
                    except Exception as e:
                        logger.warning(f"发送登出状态失败: {e}")
                
                return {
                    "ret": [ResponseCode.FAIL_SYS_SESSION_EXPIRED],
                    "data": {},
                    "msg": "登录已过期，重试和浏览器刷新都失败，请手动重新登录",
                    "error_type": "session_expired"
                }
        
        # 3. 风控检测（人机验证）
        # ✅ 使用API返回的code明确判断：RGV587_ERROR 或 FAIL_SYS_USER_VALIDATE 是风控验证（人机验证）
        # ✅ 注意：RGV587只是风控，不是封禁，可能是人机验证（滑块、九宫格等）
        # ✅ 注意：FAIL_SYS_ILLEGAL_ACCESS 已在第430行处理（非法访问，不进入此分支）
        # ✅ 封禁判断：Code包含RGV587_ERROR且URL包含bixi.alicdn.com
        # ✅ 人机验证：RGV587_ERROR + FAIL_SYS_USER_VALIDATE，需要过风控验证
        # ✅ 检查错误码是否包含 RGV587 或 FAIL_SYS_USER_VALIDATE
        has_risk_control = (
            ResponseCode.RGV587_ERROR in ret_codes or 
            any(ResponseCode.RGV587_ERROR in str(c) for c in ret_codes) or 
            "RGV587" in code_str or
            ResponseCode.FAIL_SYS_USER_VALIDATE in ret_codes or
            any(ResponseCode.FAIL_SYS_USER_VALIDATE in str(c) for c in ret_codes) or
            "FAIL_SYS_USER_VALIDATE" in code_str
        )
        if has_risk_control and not pass_risk:
            logger.warning(f"检测到风控（人机验证）: {code}")
            
            # 检查风控数据
            risk_data = result.get("data", {})
            risk_url = risk_data.get("url", "") or risk_data.get("h5url", "")
            
            # ✅ 重要：如果风控URL是登录URL，说明cookie已过期，需要登录而不是风控验证
            # 现象：清空cookie后搜索，只生成tfstk，API返回RGV587_ERROR，但url是登录iframe地址
            is_login_page = risk_url and (
                "passport.goofish.com/mini_login.htm" in risk_url or 
                ("passport.goofish.com" in risk_url and "login" in risk_url.lower())
            )
            
            if is_login_page:
                logger.warning("⚠️ 风控URL指向登录页面，判断为session过期（cookie为空或已失效）")
                
                # 尝试刷新浏览器session
                if self.browser:
                    try:
                        logger.info("尝试刷新页面自动重新登录...")
                        refresh_success = await self.browser.refresh_and_check_login()
                        
                        if refresh_success:
                            logger.success("✅ 自动刷新登录成功，重试原API请求")
                            # 重新调用原API
                            return await self.request(api, data, is_retry=True)
                        else:
                            logger.warning("自动刷新失败，登录确实已过期")
                    except Exception as e:
                        logger.error(f"刷新登录失败: {e}")
                
                # 标记登录过期，需要重新登录
                self.state.mark_logout()
                self.state.set_error("登录已过期，请重新登录")
                
                # 主动发送状态更新到控制端（通知前端登录状态已变化）
                if self.browser and hasattr(self.browser, 'tcp_client') and self.browser.tcp_client:
                    try:
                        asyncio.create_task(self.browser.tcp_client.send_status(include_cookie=False))
                    except Exception as e:
                        logger.warning(f"发送登出状态失败: {e}")
                
                return {
                    "ret": [ResponseCode.FAIL_SYS_SESSION_EXPIRED],
                    "data": {},
                    "msg": "登录已过期，请重新登录",
                    "error_type": "session_expired"
                }
            
            # ✅ 处理风控验证（人机验证）
            # ✅ 封禁判断：Code包含RGV587_ERROR且URL包含bixi.alicdn.com
            # ✅ 如果是封禁，直接返回错误，不尝试验证
            # ✅ 如果是风控验证（不是封禁），则尝试通过风控验证
            if risk_url:
                import time
                
                logger.info(f"✅ 检测到风控验证URL: {risk_url[:100]}...")
                
                # ✅ 检测是否为封禁：Code已包含RGV587_ERROR（已在此分支），URL包含bixi.alicdn.com
                is_banned = "bixi.alicdn.com" in risk_url
                
                # ✅ 如果不是封禁，统计连续风控次数
                # 注意：连续风控的定义：
                # 1. 连续3次"通过风控但下次还是触发风控"（真正的连续风控，说明账号被闲鱼标记，需要冷却）
                # 2. 连续3次"风控接口失败"（第三方接口问题，不是账号问题，不应该冷却账号）
                if not is_banned:
                    # 检查距离上次风控的时间（如果超过10分钟，重置计数）
                    last_risk_time = self.state.get("last_risk_time", 0)
                    current_time = time.time()
                    time_since_last_risk = current_time - last_risk_time if last_risk_time else float('inf')
                    
                    if time_since_last_risk > 600:  # 超过10分钟，重置计数
                        self.state.set("consecutive_risk_count", 0)
                        self.state.set("consecutive_risk_fail_count", 0)  # ✅ 同时重置失败计数
                        logger.debug("距离上次风控已超过10分钟，重置连续风控计数")
                    
                    # ✅ 先不增加计数，等待风控验证结果
                    # 如果验证成功但重试后还是触发风控，增加"通过后仍触发"的计数（这才是账号异常）
                    # 如果验证失败，记录失败次数但不影响账号状态（这是第三方接口问题）
                    # 这两种情况都需要在验证完成后处理
                    
                    # 暂时记录当前时间，用于后续判断
                    self.state.set("last_risk_time", current_time)
                
                # ✅ 如果是账号封禁，直接返回错误，不尝试验证
                if is_banned:
                    # ✅ 设置封禁状态
                    self.state.mark_banned()
                    self.state.set_work_status("banned")
                    self.state.set("last_error", "账号已被闲鱼封禁，请等待解封或联系管理员")
                    
                    # ✅ 发送状态更新到控制端
                    if self.browser and hasattr(self.browser, 'tcp_client') and self.browser.tcp_client:
                        try:
                            asyncio.create_task(self.browser.tcp_client.send_status(include_cookie=False))
                        except Exception as e:
                            logger.warning(f"发送封禁状态失败: {e}")
                    
                    logger.error(f"❌ 账号被封禁（URL包含bixi.alicdn.com），已阻止搜索和下单操作")
                    
                    return {
                        "ret": [ResponseCode.RGV587_ERROR],
                        "data": {},
                        "msg": "账号已被闲鱼封禁，请等待解封或联系管理员",
                        "error_type": "account_banned",
                        "banned": True,
                        "raw_risk_url": risk_url
                    }
                
                # ✅ 如果不是封禁，则是风控验证（人机验证），尝试通过风控验证（最多3次）
                max_risk_retries = 3
                risk_result = None
                
                for attempt in range(1, max_risk_retries + 1):
                    logger.info(f"风控验证尝试 {attempt}/{max_risk_retries}")
                    # ✅ 优先进行风控验证并同步cookie（浏览器停止不影响API使用cookie）
                    risk_result = await self._handle_risk(risk_url, attempt)
                    
                    if risk_result.get("success"):
                        # ✅ 验证成功后，优先确保cookie完全同步到state和session
                        # 这是关键：即使浏览器停止，cookie已同步到state和session，下次API请求就能使用
                        x5sec = risk_result.get("x5sec")
                        if x5sec:
                            # 确保state已更新（_handle_risk内部已更新，这里再次确认）
                            cookies = self.state.get("cookie", {})
                            if cookies.get("x5sec") != x5sec:
                                cookies["x5sec"] = x5sec
                                self.state.update(cookie=cookies)
                                logger.debug("✅ x5sec已确认更新到state")
                            
                            # 确保session已更新（这是API请求使用的cookie）
                            if self.session and not self.session.closed:
                                self.session.cookie_jar.update_cookies({"x5sec": x5sec})
                                logger.debug("✅ x5sec已确认更新到session")
                            
                            logger.success(f"✅ 风控验证成功，x5sec已同步到state和session（尝试 {attempt}）")
                        
                        # ✅ cookie同步完成后，再检查任务状态（决定是否继续重试请求）
                        work_status = self.state.get("work_status")
                        browser_status = self.state.get("browser_status")
                        
                        # ✅ 如果work_status不是searching/ordering/idle，说明任务异常或已停止
                        # 注意：不检查browser_status，因为浏览器停止不影响API使用cookie
                        if work_status not in ["searching", "ordering", "idle"]:
                            logger.warning(f"⚠️  任务已停止（work_status={work_status}），取消重试请求")
                            return {
                                "ret": [ResponseCode.RGV587_ERROR],
                                "data": {},
                                "msg": "任务已停止，取消重试请求",
                                "error_type": "task_stopped"
                            }
                        
                        # ✅ 风控验证成功，重置"连续失败"计数
                        self.state.set("consecutive_risk_fail_count", 0)
                        logger.success("✅ 风控验证成功，cookie已同步，重试请求")
                        logger.info(f"🔄 开始重试API请求: {api.api_name}")
                        
                        # ✅ 直接调用 _execute_request，不使用 request() 方法，避免重复检查和潜在的锁问题
                        try:
                            # ✅ 从当前上下文获取force_test（如果存在）
                            force_test = getattr(self, '_current_force_test', False)
                            retry_result = await self._execute_request(api, data, is_retry=True, pass_risk=True, force_test=force_test)
                            ret_code = retry_result.get('ret', ['unknown'])[0] if retry_result.get('ret') else 'no_code'
                            
                            # ✅ 检查重试后是否还是触发风控（RGV587_ERROR）
                            # 如果是，说明"通过风控但下次还是触发"，增加连续风控计数
                            if ret_code == ResponseCode.RGV587_ERROR:
                                consecutive_count = self.state.get("consecutive_risk_count", 0) + 1
                                self.state.set("consecutive_risk_count", consecutive_count)
                                logger.warning(f"⚠️ 通过风控后仍触发风控，连续风控次数: {consecutive_count}/3")
                                
                                # ✅ 如果连续3次"通过后仍触发"，说明账号异常
                                if consecutive_count >= 3:
                                    import time
                                    current_time = time.time()
                                    logger.error("=" * 70)
                                    logger.error(f"❌ 连续3次通过风控后仍触发，账号可能已被小黑屋！")
                                    logger.error("⚠️ 建议：冷却一段时间（10-30分钟）后再继续")
                                    logger.error("=" * 70)
                                    
                                    # ✅ 设置冷却时间（20分钟）
                                    cooldown_until = current_time + 1200  # 20分钟
                                    self.state.set("risk_cooldown_until", cooldown_until)
                                    self.state.set("consecutive_risk_count", 0)  # 重置计数
                                    
                                    return {
                                        "ret": [ResponseCode.RGV587_ERROR],
                                        "data": {},
                                        "msg": f"连续3次通过风控后仍触发，账号异常，建议冷却20分钟",
                                        "error_type": "risk_control_high_frequency",
                                        "consecutive_risk_count": consecutive_count,
                                        "cooldown_until": cooldown_until
                                    }
                            else:
                                # ✅ 重试成功（没有触发风控），重置连续风控计数
                                self.state.set("consecutive_risk_count", 0)
                            
                            logger.info(f"🔄 重试API请求完成: {api.api_name}, code={ret_code}, msg={retry_result.get('msg', 'N/A')[:100]}")
                            return retry_result
                        except asyncio.TimeoutError:
                            logger.error(f"❌ 重试API请求超时: {api.api_name}")
                            return {
                                "ret": [ResponseCode.FAIL_SYS_REQUEST_TIMEOUT],
                                "data": {},
                                "msg": "重试请求超时",
                                "error_type": "retry_timeout"
                            }
                        except Exception as e:
                            logger.exception(f"❌ 重试API请求异常: {api.api_name}, error: {e}")
                            return {
                                "ret": [ResponseCode.RGV587_ERROR],
                                "data": {},
                                "msg": f"重试请求异常: {e}",
                                "error_type": "retry_error"
                            }
                    else:
                        if attempt < max_risk_retries:
                            logger.warning(f"风控验证失败（第{attempt}次），等待2秒后重试...")
                            await asyncio.sleep(2)
                        else:
                            logger.error(f"风控验证失败（已尝试{max_risk_retries}次）")
                
                # ✅ 所有尝试都失败，增加"连续失败"计数（仅用于日志记录，不触发冷却）
                # 注意：风控接口失败是第三方接口问题，不是账号问题，不应该冷却账号
                consecutive_fail_count = self.state.get("consecutive_risk_fail_count", 0) + 1
                self.state.set("consecutive_risk_fail_count", consecutive_fail_count)
                
                error_msg = risk_result.get("msg", "触发风控验证失败")
                risk_raw = risk_result.get("raw_response")
                
                # ✅ 如果连续3次都失败，记录警告但不冷却账号（这是第三方接口问题）
                if consecutive_fail_count >= 3:
                    logger.warning("=" * 70)
                    logger.warning(f"⚠️ 连续3次风控接口调用失败，可能是第三方接口不稳定！")
                    logger.warning(f"⚠️ 这不会影响账号状态，但建议检查风控接口配置或联系接口提供商")
                    logger.warning("=" * 70)
                    
                    # ✅ 重置失败计数，避免后续请求一直被警告
                    # 但不清除冷却期，因为这不是账号问题
                    self.state.set("consecutive_risk_fail_count", 0)
                    
                    # ✅ 返回错误，但不设置冷却期
                    return {
                        "ret": [ResponseCode.RGV587_ERROR],
                        "data": {},
                        "msg": f"连续3次风控接口失败，可能是第三方接口不稳定，请检查配置",
                        "error_type": "risk_api_failure",
                        "consecutive_risk_fail_count": consecutive_fail_count
                    }
                
                logger.warning(f"⚠️  风控验证失败（已尝试{max_risk_retries}次），连续失败次数: {consecutive_fail_count}/3（第三方接口问题，不影响账号状态），跳过本次搜索，等待下次搜索任务")
                error_response = {
                    "ret": [ResponseCode.RGV587_ERROR],
                    "data": {},
                    "msg": f"触发风控验证失败: {error_msg}",
                    "error_type": "risk_control",
                    "raw_risk_response": risk_raw,  # 返回完整风控接口响应，便于调试
                    "consecutive_risk_fail_count": consecutive_fail_count
                }
                return error_response
            else:
                logger.error("风控数据缺失")
                return {
                    "ret": [ResponseCode.RGV587_ERROR],
                    "data": {},
                    "msg": "触发风控但缺少验证URL",
                    "error_type": "risk_control"
                }
        
        # 4. 其他错误码，统一返回错误信息
        if code and not (code.startswith("SUCCESS") or code == ResponseCode.SUCCESS):
            error_msg = result.get("msg", f"API调用失败: {code}")
            
            # ✅ 识别业务错误（不应该影响账号状态）
            code_str = str(code)
            is_business_error = (
                ResponseCode.FAIL_BIZ_PC_CAN_NOT_TRADE in code_str or
                ResponseCode.FAIL_BIZ_ALL_INVENTORY_WITH_HOLDING in code_str or
                ResponseCode.FAIL_BIZ_ITEM_CANNOT_BUY_ERROR in code_str or
                ResponseCode.FAIL_BIZ_DEFAULT_INVENTORY_NOT_ENOUGH in code_str
            )
            
            error_type = "business_error" if is_business_error else "api_error"
            
            return {
                "ret": ret_codes,
                "data": {},
                "msg": error_msg,
                "error_type": error_type,
                "api_code": code,
                "is_business_error": is_business_error
            }
        
        return result
    
    async def _refresh_token(self) -> Dict[str, Any]:
        """
        刷新token - 简化版本
        
        关键发现：删除token后直接请求，服务器会自动返回新token（前提是Session有效）
        
        流程：
        1. 清理本地token（从state和cookie_jar中删除）
        2. 使用空token请求首页API
        3. 服务器检测到token为空但Session有效，自动返回新token
        4. 从响应中提取新token
        
        Returns:
            {"success": bool, "token_changed": bool, "msg": str}
        """
        try:
            logger.info("🔄 开始刷新token（删除token后重新请求）...")
            
            # ✅ 先清理本地token，确保请求时token为空
            current_cookies = self.state.get("cookie", {})
            old_token_full = current_cookies.get("_m_h5_tk", "")
            old_token = old_token_full.split('_')[0] if old_token_full else ""
            
            # 清理本地token
            current_cookies.pop('_m_h5_tk', None)
            current_cookies.pop('_m_h5_tk_enc', None)
            self.state.update(cookie=current_cookies)
            logger.debug("✅ 已清理本地token，准备重新请求获取新token")
            
            # ✅ 清理session的cookie_jar中的token（使用 clear_domain 方法）
            # 关键：aiohttp 会同时发送手动设置的 Cookie 头和 cookie_jar 中的 cookie
            # 如果 cookie_jar 中有旧 token，会导致 Cookie 冲突，服务器返回"非法请求"
            main_session = await self.get_session()
            try:
                # ✅ 方法1：清空整个 cookie_jar（最彻底）
                # 注意：清空后，服务器返回的新 cookie 会自动添加到 cookie_jar
                main_session.cookie_jar.clear()
                logger.debug("✅ 已清空 cookie_jar（防止 Cookie 冲突导致非法请求）")
                
                # ✅ 方法2（备选）：只清空特定域名的 cookie
                # main_session.cookie_jar.clear_domain(".m.goofish.com")
                # main_session.cookie_jar.clear_domain("m.goofish.com")
                # logger.debug("✅ 已清空 cookie_jar 中的闲鱼域名 cookie（防止 Cookie 冲突）")
            except Exception as e:
                # ⚠️ 清理失败会影响请求！因为会导致 Cookie 冲突
                logger.error(f"❌ 清理 cookie_jar 失败，可能导致 Cookie 冲突和非法请求: {e}")
                # 尝试备用方案：遍历删除
                try:
                    # 遍历并删除 token 相关的 cookie
                    cookies_to_remove = []
                    for cookie in main_session.cookie_jar:
                        if cookie.key in ['_m_h5_tk', '_m_h5_tk_enc']:
                            cookies_to_remove.append((cookie.key, cookie['domain'], cookie['path']))
                    
                    for key, domain, path in cookies_to_remove:
                        main_session.cookie_jar.clear(domain=domain, path=path, key=key)
                    
                    if cookies_to_remove:
                        logger.debug(f"✅ 备用方案：已删除 {len(cookies_to_remove)} 个 token cookie")
                except Exception as e2:
                    logger.error(f"❌ 备用方案也失败: {e2}")
                    logger.error("⚠️ 警告：cookie_jar 未清理，可能导致非法请求！")
            
            # ✅ 说明：cookie_jar中的token已被删除，不会影响请求，因为：
            # 1. 我们手动构建Cookie头时，从state获取cookie（state中已经在第1250-1253行pop掉了token）
            # 2. cookie_jar中的token也已在上方删除（第1260-1281行），保持一致性
            # 3. 如果state中没有token，我们会在构建Cookie头时手动添加空的_m_h5_tk=，确保服务器识别为"令牌为空"
            # 4. 服务器返回新token后，cookie_jar会自动更新，我们也会从cookie_jar中提取并更新到state
            # 5. 后续请求时，state中已有新token，手动构建的Cookie头会包含新token
            
            # ✅ 直接用空token请求首页API（服务器会根据Session有效性返回新token）
            # 关键发现：删除token后直接请求，服务器会自动返回新token（前提是Session有效）
            logger.info("通过首页API mtop.gaia.nodejs.gaia.idle.data.gw.v2.index.get 刷新token...")
            
            # ✅ 按照原始请求，data应该是 {"piUrl":"https://h5.m.goofish.com/wow/moyu/moyu-project/xy-site/pages/announcement"}
            request_data_dict = {
                "piUrl": "https://h5.m.goofish.com/wow/moyu/moyu-project/xy-site/pages/announcement"
            }
            request_data = json.dumps(request_data_dict, ensure_ascii=False)
            timestamp = self.get_timestamp()
            
            # ✅ 使用空token构建签名（已清理token，使用空字符串）
            # ✅ 关键修复：使用与 build_sign 相同的签名计算方式，确保一致性
            # 关键发现：删除token后直接请求，服务器会自动返回新token
            # 使用 build_sign 方法确保签名计算一致（即使token为空）
            sign = self.build_sign(timestamp, request_data)
            logger.debug("✅ 使用空token构建签名（token已清理，使用build_sign方法确保一致性）")
            url = f"{self.HOST_URL}/{GoofishAPIs.HOME_INDEX.api_name}/{GoofishAPIs.HOME_INDEX.version}/?"
            url_params = [
                "jsv=2.7.2",
                f"appKey={self.APP_KEY}",
                f"t={timestamp}",
                f"dataType={GoofishAPIs.HOME_INDEX.data_type}",
                f"type={GoofishAPIs.HOME_INDEX.format_type}",
                f"api={GoofishAPIs.HOME_INDEX.api_name}",
                "timeout=20000",
                "sessionOption=AutoLoginOnly",
                # ✅ 可选：测试发现删除token后直接请求也能返回新token，此参数可能不是必需的
                # "dangerouslySetWindvaneParams=%5Bobject%20Object%5D",
                "accountSite=xianyu",
                f"sign={sign}",
                f"v={GoofishAPIs.HOME_INDEX.version}",
                f"spm_cnt={GoofishAPIs.HOME_INDEX.spm_cnt}"
            ]
            if GoofishAPIs.HOME_INDEX.spm_pre:
                url_params.append(f"spm_pre={GoofishAPIs.HOME_INDEX.spm_pre}")
            url += "&".join(url_params)
            
            headers = self.build_headers()
            # ✅ 直接使用主session（与搜索、下单共享同一个session），这样cookie会自动更新
            # ✅ 主session会自动处理cookie的更新，无需手动同步
            main_session = await self.get_session()
            post_data = f"data={quote(request_data)}"
            headers["Content-Type"] = "application/x-www-form-urlencoded"
            
            # ✅ 关键修复：与 _execute_request 保持一致，手动构建Cookie头
            # 原因：确保所有cookie都正确传递，避免cookie_jar自动处理导致的问题
            cookies = self.state.get("cookie", {})
            
            # ✅ 检查关键cookie是否存在（cookie2和unb是Session的关键标识）
            key_session_cookies = ['cookie2', 'unb', '_tb_token_']
            missing_session_cookies = [k for k in key_session_cookies if k not in cookies or not cookies.get(k)]
            if missing_session_cookies:
                logger.error(f"❌ Token刷新失败：缺少关键Session Cookie: {missing_session_cookies}")
                logger.error(f"   当前cookie列表: {list(cookies.keys())}")
                logger.error(f"   💡 建议：Session Cookie已失效，需要浏览器刷新页面重新获取cookie")
                return {
                    "success": False,
                    "msg": f"缺少关键Session Cookie: {missing_session_cookies}，需要浏览器刷新",
                    "session_expired": True,  # 标记为Session过期，触发浏览器刷新
                    "illegal_access": False
                }
            
            cookie_header_parts = []
            if cookies:
                for name, value in cookies.items():
                    cookie_value = value if value is not None else ""
                    cookie_header_parts.append(f"{name}={cookie_value}")
            
            # ✅ 确保_m_h5_tk始终存在（即使为空），用于标识"令牌为空"
            if '_m_h5_tk' not in cookies or not cookies.get('_m_h5_tk'):
                has_token_header = any(part.startswith('_m_h5_tk=') for part in cookie_header_parts)
                if not has_token_header:
                    cookie_header_parts.append("_m_h5_tk=")
            
            # ✅ 手动设置Cookie请求头（与 _execute_request 保持一致）
            if cookie_header_parts:
                headers["Cookie"] = "; ".join(cookie_header_parts)
            else:
                headers["Cookie"] = ""
            
            cookies_dict = {}  # 初始化cookie字典
            
            # ✅ 使用主session发送请求（cookie会自动更新到session的cookie jar中）
            # 关键发现：删除token后直接请求，服务器会自动返回新token（前提是Session有效）
            async with main_session.post(url, data=post_data, headers=headers, timeout=10) as response:
                response_text = await response.text()
                
                # ✅ 检查响应码，但不作为是否成功的判断（因为不带token时返回"令牌为空"是正常的）
                ret_codes_info = None
                try:
                    response_json = json.loads(response_text)
                    ret_codes = response_json.get("ret", [])
                    if ret_codes:
                        ret_codes_info = ret_codes
                        if any(code.startswith("SUCCESS") for code in ret_codes):
                            logger.debug("首页API调用成功，已获取到token")
                        elif any("FAIL_SYS_TOKEN_EMPTY" in str(code) for code in ret_codes):
                            # ✅ 不带token时返回"令牌为空"是正常的，继续从cookie中取新token
                            logger.debug("首页API返回'令牌为空'（这是正常的，继续从cookie中取新token）")
                        elif any("FAIL_SYS_TOKEN_EXPIRED" in str(code) for code in ret_codes):
                            # ✅ 带token刷新时返回"令牌过期"也是正常的，继续从cookie中取新token
                            logger.debug("首页API返回'令牌过期'（这是正常的，继续从cookie中取新token）")
                        elif any("FAIL_SYS_ILLEGAL_ACCESS" in str(code) for code in ret_codes):
                            # ❌ 非法请求：通常是Cookie缺失或混乱导致的
                            logger.error(f"❌ Token刷新时返回非法请求: {ret_codes}")
                            logger.error("⚠️  非法请求通常原因：Cookie缺失或混乱（Session Cookie已失效）")
                            
                            # ✅ 检查关键Cookie是否存在
                            cookies_check = self.state.get("cookie", {})
                            key_cookies_check = ['cookie2', 'unb']
                            missing_cookies_check = [k for k in key_cookies_check if k not in cookies_check or not cookies_check.get(k)]
                            
                            if missing_cookies_check:
                                # ❌ 关键Session Cookie缺失，必须刷新浏览器
                                logger.error(f"  ❌ 缺少关键Session Cookie: {missing_cookies_check}")
                                logger.error(f"  💡 建议：Session Cookie已失效，需要浏览器刷新页面重新获取cookie")
                                return {
                                    "success": False,
                                    "msg": f"Token刷新返回非法请求：缺少关键Session Cookie {missing_cookies_check}，需要浏览器刷新",
                                    "session_expired": True,  # 标记为Session过期，触发浏览器刷新
                                    "illegal_access": False
                                }
                            else:
                                # ⚠️ Cookie存在但返回非法请求，可能是Cookie已失效或混乱
                                # 注意：这种情况通常表示Session Cookie已失效，需要浏览器刷新
                                # 但为了确保，我们标记为需要浏览器刷新（因为已经尝试过刷新token了）
                                logger.error(f"  ⚠️  Session Cookie存在但返回非法请求，Cookie可能已失效或混乱")
                                logger.error(f"  💡 已尝试刷新token但失败，需要浏览器刷新页面重新获取有效的cookie")
                                return {
                                    "success": False,
                                    "msg": f"Token刷新返回非法请求：Session Cookie可能已失效，需要浏览器刷新",
                                    "session_expired": True,  # 标记为需要浏览器刷新（已尝试过刷新token）
                                    "illegal_access": False
                                }
                        elif any("FAIL_SYS_SESSION_EXPIRED" in str(code) for code in ret_codes):
                            # ⚠️ Session过期：需要浏览器刷新恢复登录
                            logger.warning(f"首页API返回Session过期: {ret_codes}")
                            logger.warning("Session已过期，需要浏览器刷新页面恢复登录")
                            return {
                                "success": False,
                                "msg": f"Token刷新返回Session过期: {ret_codes}，需要浏览器刷新",
                                "session_expired": True,  # 标记为Session过期，触发浏览器刷新
                                "illegal_access": False
                            }
                        else:
                            logger.warning(f"首页API返回: {ret_codes}")
                except:
                    pass  # 即使解析失败，也继续尝试获取cookie
                
                # ✅ 从主session的cookie jar中提取所有相关cookie（自动更新的cookie）
                # 根据文章介绍，mtop.gaia.nodejs.gaia.idle.data.gw.v2.index.get 返回4个cookie：
                # 1. _m_h5_tk - token明文
                # 2. _m_h5_tk_enc - 加密后的token，用于服务器验证（非对称密钥的公钥加密）
                # 3. cookie2 - 用于验证
                # 4. mtop_partitioned_detect - 分区检测
                for cookie in main_session.cookie_jar:
                    cookies_dict[cookie.key] = cookie.value

                # ✅ 记录提取到的关键cookie（用于调试）
                important_cookies = {
                    '_m_h5_tk': cookies_dict.get('_m_h5_tk', '未获取'),
                    '_m_h5_tk_enc': cookies_dict.get('_m_h5_tk_enc', '未获取'),
                    'cookie2': cookies_dict.get('cookie2', '未获取'),
                    'mtop_partitioned_detect': cookies_dict.get('mtop_partitioned_detect', '未获取')
                }
                logger.debug(f"🔍 刷新token提取的cookie（来自主session）: {important_cookies}")
            
            # × 再次尝试从浏览器同步（API调用可能触发了浏览器更新cookie）
            # API调用怎么会触发浏览器更新cookie？浏览器cookie变成是旧的一套token和session，这样反而覆盖了刚获取的cookie吧
            # if self.browser:
            #     try:
            #         browser_cookies = await self.browser.get_cookie_dict()
            #         if browser_cookies:
            #             cookies_dict.update(browser_cookies)
            #     except:
            #         pass
            
            # ⚠️ 关键：必须同时获取到4个关键cookie，特别是_m_h5_tk_enc（用于服务器验证_m_h5_tk有效性）
            # todo:如果session没过期，cookie2可能不会返回,cookie2不一定在返回中,但是这个missing_key_cookies没见触发过
            key_cookies_required = ['_m_h5_tk', '_m_h5_tk_enc', 'cookie2', 'mtop_partitioned_detect']
            missing_key_cookies = [k for k in key_cookies_required if k not in cookies_dict or not cookies_dict.get(k)]
            
            if missing_key_cookies:
                logger.error(f"❌ Token刷新失败：缺少关键cookie: {missing_key_cookies}")
                logger.error(f"   已获取的cookie: {list(cookies_dict.keys())}")
                # 如果缺少_m_h5_tk或_m_h5_tk_enc，返回失败
                if '_m_h5_tk' in missing_key_cookies or '_m_h5_tk_enc' in missing_key_cookies:
                    return {
                        "success": False,
                        "msg": f"Token刷新失败：缺少关键cookie {missing_key_cookies}",
                        "session_expired": False
                    }
                else:
                    # cookie2或mtop_partitioned_detect缺失，警告但继续
                    logger.warning(f"⚠️  部分cookie缺失: {missing_key_cookies}，继续执行")
            
            # ✅ 检查token是否存在且不为空字符串（不能为None）
            token_value = cookies_dict.get('_m_h5_tk', '')
            if token_value and token_value.strip():
                new_token_full = token_value
                new_token = new_token_full.split('_')[0]
                
                # ✅ 验证_m_h5_tk_enc是否存在且有效（必须！否则会导致非法请求）
                token_enc_value = cookies_dict.get('_m_h5_tk_enc', '')
                if not token_enc_value or token_enc_value.strip() == "":
                    logger.error("❌ Token刷新失败：_m_h5_tk_enc缺失（此cookie用于服务器验证_m_h5_tk有效性，缺失会导致非法请求）")
                    return {
                        "success": False,
                        "msg": "Token刷新失败：_m_h5_tk_enc缺失，会导致非法请求",
                        "session_expired": False,
                        "illegal_access": True  # 标记可能导致非法请求
                    }
                
                # ✅ 对比token是否真的有变化
                token_changed = (old_token_full != new_token_full) and (old_token != new_token)
                
                # ✅ 验证_m_h5_tk_enc是否也变化了（必须匹配，否则会导致非法请求）
                old_token_enc = self.state.get("cookie", {}).get("_m_h5_tk_enc", "")
                new_token_enc = cookies_dict.get("_m_h5_tk_enc", "")
                token_enc_changed = (old_token_enc != new_token_enc)
                
                # ⚠️ 关键检查：如果_m_h5_tk变化了但_m_h5_tk_enc没变化，会导致非法请求
                if token_changed and not token_enc_changed:
                    logger.error("❌ Token刷新异常：_m_h5_tk已变化但_m_h5_tk_enc未变化，会导致非法请求！")
                    logger.error(f"   旧token: {old_token[:20]}..., 新token: {new_token[:20]}...")
                    logger.error(f"   _m_h5_tk_enc未变化: {old_token_enc[:20] if old_token_enc else '(空)'}...")
                    return {
                        "success": False,
                        "msg": "Token和加密Token不匹配，会导致非法请求",
                        "session_expired": False,
                        "illegal_access": True  # 标记可能导致非法请求
                    }
                
                if token_changed:
                    logger.debug(f"✅ Token已变化: 旧={old_token[:20] if old_token and old_token != 'undefined' else 'undefined'}... -> 新={new_token[:20]}...")
                    logger.debug(f"✅ _m_h5_tk_enc已变化: {token_enc_changed}")
                elif old_token_full and old_token != 'undefined':
                    logger.debug(f"Token未变化（但仍使用新token）: {new_token[:20]}...")
                
                # ✅ 更新state中的cookie（合并所有cookie）
                # ✅ 重要：确保4个关键cookie都被更新：_m_h5_tk, _m_h5_tk_enc, cookie2, mtop_partitioned_detect
                current_cookies = self.state.get("cookie", {})
                current_cookies.update(cookies_dict)  # 更新所有cookie，包括_m_h5_tk、_m_h5_tk_enc、cookie2、mtop_partitioned_detect等
                self.state.update(cookie=current_cookies)
                
                # ✅ 验证关键cookie是否都已更新到state
                key_cookies_updated = [k for k in key_cookies_required if k in current_cookies and current_cookies.get(k)]
                if len(key_cookies_updated) != len(key_cookies_required):
                    missing_updated = [k for k in key_cookies_required if k not in key_cookies_updated]
                    logger.error(f"❌ 关键cookie未更新到state: {missing_updated}")
                    return {
                        "success": False,
                        "msg": f"关键cookie未更新到state: {missing_updated}",
                        "session_expired": False
                    }
                
                # ✅ 重要：确保get_token()能获取到新token
                # ✅ 验证_m_h5_tk_enc已正确更新（这是关键，用于服务器验证）
                token_enc_after_update = current_cookies.get("_m_h5_tk_enc", "")
                if not token_enc_after_update:
                    logger.error("❌ Token刷新后，state中的_m_h5_tk_enc仍为空")
                    return {
                        "success": False,
                        "msg": "Token刷新后，_m_h5_tk_enc未更新到state",
                        "session_expired": False
                    }
                logger.debug(f"✅ 已验证_m_h5_tk_enc已更新: {token_enc_after_update[:30]}...")
                
                # ✅ 重要：确保get_token()能获取到新token
                logger.info(f"🔍 Token刷新后验证: state中的token={self.get_token()[:30] if self.get_token() != 'undefined' else 'undefined'}...")
                
                # ✅ 主session的cookie jar已经自动更新（因为使用了主session发送请求）
                # ✅ 验证主session的cookie jar是否已包含关键cookie
                try:
                    session_cookies = {cookie.key: cookie.value for cookie in main_session.cookie_jar}
                    key_cookies_in_session = {
                        '_m_h5_tk': session_cookies.get('_m_h5_tk', '未更新'),
                        '_m_h5_tk_enc': session_cookies.get('_m_h5_tk_enc', '未更新'),
                        'cookie2': session_cookies.get('cookie2', '未更新'),
                        'mtop_partitioned_detect': session_cookies.get('mtop_partitioned_detect', '未更新')
                    }
                    logger.debug(f"✅ 主session的cookie jar已自动更新: {key_cookies_in_session}")
                except Exception as e:
                    logger.warning(f"验证主session cookie jar失败: {e}")
                
                # ✅ 同步4个关键cookie到浏览器和控制端
                try:
                    # 同步到浏览器
                    if self.browser:
                        key_cookies_to_sync = {
                            '_m_h5_tk': cookies_dict.get('_m_h5_tk', ''),
                            '_m_h5_tk_enc': cookies_dict.get('_m_h5_tk_enc', ''),
                            'cookie2': cookies_dict.get('cookie2'),
                            'mtop_partitioned_detect': cookies_dict.get('mtop_partitioned_detect')
                        }
                        # 只同步存在的cookie
                        key_cookies_to_sync = {k: v for k, v in key_cookies_to_sync.items() if v}
                        if key_cookies_to_sync:
                            # 批量同步cookie到浏览器
                            for cookie_name, cookie_value in key_cookies_to_sync.items():
                                await self._sync_cookie(cookie_name, cookie_value)
                            logger.debug(f"✅ 已同步关键cookie到浏览器: {list(key_cookies_to_sync.keys())}")
                    
                    # 同步到控制端（通过TCP客户端发送状态更新）
                    if self.browser and hasattr(self.browser, 'tcp_client') and self.browser.tcp_client:
                        try:
                            asyncio.create_task(self.browser.tcp_client.send_status(include_cookie=True))
                            logger.debug("✅ 已发送cookie更新到控制端")
                        except Exception as e:
                            logger.warning(f"发送cookie更新到控制端失败: {e}")
                except Exception as e:
                    logger.warning(f"同步cookie到浏览器和控制端失败: {e}")
                
                # ⚠️ 如果Token没有变化，说明API刷新失败，需要使用浏览器刷新页面
                if not token_changed:
                    logger.warning("⚠️  Token刷新后未变化，可能是Session过期，尝试使用浏览器刷新页面...")
                    
                    # ✅ 使用浏览器刷新页面提取cookie（最保底的做法）
                    if self.browser:
                        try:
                            logger.info("🔄 使用浏览器刷新personal页面并提取cookie...")
                            # 访问personal页面并等待稳定
                            await self.browser.page.goto("https://www.goofish.com/personal", wait_until="domcontentloaded", timeout=15000)
                            await asyncio.sleep(2)  # 等待页面稳定
                            
                            # 提取所有cookie
                            browser_cookies = await self.browser.get_cookie_dict()
                            if browser_cookies:
                                cookies_dict.update(browser_cookies)
                                
                                # 检查是否有新token
                                new_token_full_after_refresh = cookies_dict.get("_m_h5_tk", "")
                                if new_token_full_after_refresh and new_token_full_after_refresh != old_token_full:
                                    logger.success("✅ 浏览器刷新后获取到新token")
                                    # 重新提取token信息
                                    new_token_full = new_token_full_after_refresh
                                    new_token = new_token_full.split('_')[0]
                                    token_changed = True
                                    # 更新state
                                    current_cookies.update(cookies_dict)
                                    self.state.update(cookie=current_cookies)
                                else:
                                    logger.error("❌ 浏览器刷新后token仍未变化，Session可能已完全过期")
                                    return {
                                        "success": False,
                                        "msg": "Token刷新失败，Session已过期，请重新登录",
                                        "session_expired": True
                                    }
                        except Exception as e:
                            logger.error(f"浏览器刷新页面失败: {e}")
                            return {
                                "success": False,
                                "msg": f"Token刷新失败，浏览器刷新异常: {e}",
                                "session_expired": True
                            }
                    else:
                        logger.error("❌ Token未变化且无浏览器可用，无法刷新")
                        return {
                            "success": False,
                            "msg": "Token刷新失败，Session已过期",
                            "session_expired": True
                        }
                
                logger.success(f"✅ Token刷新成功: {new_token[:20]}... (变化: {token_changed})")
                logger.debug(f"✅ 关键cookie已同步: _m_h5_tk={bool(current_cookies.get('_m_h5_tk'))}, "
                           f"_m_h5_tk_enc={bool(current_cookies.get('_m_h5_tk_enc'))}, "
                           f"cookie2={bool(current_cookies.get('cookie2'))}, "
                           f"mtop_partitioned_detect={bool(current_cookies.get('mtop_partitioned_detect'))}")
                
                # ✅ 等待一小段时间让新token生效
                await asyncio.sleep(1)
                
                return {
                    "success": True,
                    "msg": "Token刷新成功",
                    "token_changed": token_changed,
                    "token_enc_changed": token_enc_changed,
                    "session_expired": False
                }
            else:
                logger.error("Token刷新失败：未获取到_m_h5_tk")
                # ✅ 备选方案：如果首页API失败，尝试从浏览器获取（可能token已经在浏览器中更新了）
                if self.browser:
                    try:
                        logger.debug("首页API未获取到token，尝试从浏览器获取...")
                        browser_cookies = await self.browser.get_cookie_dict()
                        if browser_cookies and '_m_h5_tk' in browser_cookies:
                            current_cookies = self.state.get("cookie", {})
                            current_cookies['_m_h5_tk'] = browser_cookies['_m_h5_tk']
                            self.state.update(cookie=current_cookies)
                            
                            # 更新session的cookie jar
                            try:
                                session = await self.get_session()
                                session.cookie_jar.update_cookies({'_m_h5_tk': browser_cookies['_m_h5_tk']})
                            except Exception as e:
                                logger.warning(f"更新session cookie jar失败: {e}")
                            
                            logger.success(f"✅ 从浏览器获取到token: {browser_cookies['_m_h5_tk'][:20]}...")
                            await asyncio.sleep(1)
                            return {"success": True, "msg": "Token已从浏览器获取"}
                    except Exception as e:
                        logger.warning(f"从浏览器获取token失败: {e}")
                
                return {"success": False, "msg": "未获取到新token"}
                    
        except Exception as e:
            logger.error(f"Token刷新异常: {e}", exc_info=True)
            return {"success": False, "msg": str(e)}
    
    async def _sync_cookie(self, cookie_name: str, cookie_value: str, domain: str = None) -> bool:
        """
        通用的单个cookie同步方法，用于同步单个cookie到state、session、浏览器和控制端
        
        Args:
            cookie_name: cookie名称（如 'x5sec', 'havana_lgc2_77'）
            cookie_value: cookie值
            domain: cookie的domain（如 'goofish.com'），如果为None则自动从当前页面获取
        
        Returns:
            bool: 同步是否成功
        """
        try:
            # 获取当前cookies
            cookies = self.state.get("cookie", {})
            
            # ✅ 1. 更新state中的cookie（本地持久化）
            cookies[cookie_name] = cookie_value
            self.state.update(cookie=cookies)
            logger.debug(f"✅ {cookie_name}已更新到state")
            
            # ✅ 2. 更新session中的cookie（当前请求会话）
            if self.session and not self.session.closed:
                self.session.cookie_jar.update_cookies({cookie_name: cookie_value})
                logger.debug(f"✅ {cookie_name}已更新到session")
            
            # ✅ 3. 同步cookie到浏览器（确保浏览器也持有最新的cookie）
            if self.browser:
                try:
                    browser_context = None
                    if hasattr(self.browser, 'browser') and self.browser.browser:
                        browser_context = self.browser.browser
                    
                    if not browser_context:
                        logger.debug(f"⚠️  无法获取浏览器上下文，跳过{cookie_name}同步到浏览器")
                    else:
                        # 获取domain（如果没有指定，则从当前页面获取）
                        if not domain:
                            domain = ".goofish.com"  # 默认domain（使用点开头的可以匹配所有子域名）
                            if hasattr(self.browser, 'page') and self.browser.page and not self.browser.page.is_closed():
                                try:
                                    current_url = self.browser.page.url
                                    from urllib.parse import urlparse
                                    parsed = urlparse(current_url)
                                    if parsed.netloc:
                                        netloc = parsed.netloc.split(':')[0]  # 移除端口号
                                        # 对于goofish.com，使用.goofish.com
                                        if 'goofish.com' in netloc:
                                            domain = ".goofish.com"
                                        else:
                                            domain = netloc
                                except Exception as e:
                                    logger.debug(f"获取页面URL失败: {e}，使用默认domain")
                        
                        # ✅ 设置cookie到浏览器
                        await browser_context.add_cookies([{
                            "name": cookie_name,
                            "value": cookie_value,
                            "domain": domain,
                            "path": "/"
                        }])
                        logger.debug(f"✅ {cookie_name}已同步到浏览器 (domain: {domain})")
                except Exception as e:
                    logger.warning(f"同步{cookie_name}到浏览器失败: {e}（不影响使用）")
            
            # ✅ 4. 发送状态更新到控制端（包含cookie）
            if self.browser and hasattr(self.browser, 'tcp_client') and self.browser.tcp_client:
                try:
                    # ✅ 检查TCP客户端是否已连接
                    if hasattr(self.browser.tcp_client, 'writer') and self.browser.tcp_client.writer:
                        asyncio.create_task(self.browser.tcp_client.send_status(include_cookie=True))
                        logger.debug(f"✅ {cookie_name}已同步到控制端")
                    else:
                        logger.debug(f"TCP客户端未连接，跳过同步{cookie_name}到控制端")
                except Exception as e:
                    logger.debug(f"同步{cookie_name}到控制端失败: {e}")
            
            return True
        except Exception as e:
            logger.error(f"同步{cookie_name}失败: {e}", exc_info=True)
            return False
    
    async def _handle_risk(self, risk_url: str, attempt: int = 1) -> Dict[str, Any]:
        """
        处理风控验证
        调用第三方风控验证API获取x5sec
        Args:
            risk_url: 风控验证URL
            attempt: 重试次数（用于日志）
        Returns:
            {
                "success": bool,
                "msg": str,
                "x5sec": str or None,
                "raw_response": dict or None  # 完整的风控API响应，便于调试
            }
        """
        try:
            logger.warning(f"触发风控，验证URL: {risk_url} (尝试 {attempt})")
            
            # 检查风控API配置
            from utils import configs
            if not configs.SLIDER_API_URL or not configs.SLIDER_API_KEY:
                logger.error("未配置风控API，无法自动验证")
                return {
                    "success": False,
                    "msg": "未配置风控API",
                    "x5sec": None,
                    "raw_response": None
                }
            
            # 获取当前cookie并转换为字符串格式
            cookies = self.state.get("cookie", {})
            if not cookies:
                logger.error("Cookie为空，无法进行风控验证")
                return {
                    "success": False,
                    "msg": "Cookie为空",
                    "x5sec": None,
                    "raw_response": None
                }
            
            # 将cookie字典转换为字符串格式: "name1=value1; name2=value2"
            # cookie_str = "; ".join([f"{k}={v}" for k, v in cookies.items()])
            
            # 调用风控验证API（同步方法，在executor中运行避免阻塞）
            logger.info(f"正在调用风控验证API（尝试 {attempt}）...")
            loop = asyncio.get_event_loop()
            risk_result = await loop.run_in_executor(None, RiskAPI.get_x5sec, risk_url, cookies)
            
            # risk_result 现在是结构化结果 {success, msg, x5sec, raw_response}
            if risk_result.get("success") and risk_result.get("x5sec"):
                x5sec = risk_result["x5sec"]
                # 验证成功，更新x5sec到所有节点
                logger.success(f"✅ 风控验证成功（尝试 {attempt}），x5sec: {x5sec[:20]}...")
                
                # ✅ 使用通用方法同步x5sec到所有节点
                await self._sync_cookie("x5sec", x5sec)
                logger.info(f"✅ x5sec已同步到所有节点（state、session、浏览器、控制端）")
                
                return {
                    "success": True,
                    "msg": "风控验证成功",
                    "x5sec": x5sec,
                    "raw_response": risk_result.get("raw_response")
                }
            else:
                # 验证失败，返回完整信息
                error_msg = risk_result.get("msg", "风控验证失败，未获取到x5sec")
                logger.error(f"❌ 风控验证失败（尝试 {attempt}）: {error_msg}")
                return {
                    "success": False,
                    "msg": error_msg,
                    "x5sec": None,
                    "raw_response": risk_result.get("raw_response")  # 返回完整响应便于调试
                }
            
        except Exception as e:
            logger.error(f"风控处理异常: {e}")
            return {
                "success": False,
                "msg": f"风控验证异常: {str(e)}",
                "x5sec": None,
                "raw_response": None
            }
    
    # ========== 具体API方法 ========== #
    
    async def search(self, keyword: str, page: int = 1, **kwargs) -> Dict[str, Any]:
        """
        搜索商品
        Args:
            keyword: 搜索关键词
            page: 页码
            **kwargs: 其他参数
                - min_price: 最低价格
                - max_price: 最高价格
                - sortValue: 排序值（asc/desc，默认 "desc"）
                - sortField: 排序字段（默认 "create"）
                - fromFilter: 是否来自筛选
                - rowsPerPage: 每页数量（默认 30）
        """
        # 价格过滤逻辑：当 min/max 有设置时启用 fromFilter，并按闲鱼格式传入 searchFilter
        # ✅ 根据浏览器实际请求：
        #    - 起始价格未设置时为 0（字符串 "0"）
        #    - 最大价格未设置时为 undefined（字符串 "undefined"）
        min_price_val = kwargs.get("min_price")
        max_price_val = kwargs.get("max_price")
        
        # 判断是否使用了价格筛选（任一价格不为 None）
        use_filter = (min_price_val is not None) or (max_price_val is not None)
        
        if use_filter:
            # 起始价格：未设置（None）或值为 0 时，都使用 "0"
            if min_price_val is None:
                min_text = "0"
            else:
                min_price = int(min_price_val)
                min_text = "0" if min_price == 0 else str(min_price)
            
            # 最大价格：未设置（None）时使用 "undefined"，值为 0 时也使用 "undefined"
            if max_price_val is None:
                max_text = "undefined"
            else:
                max_price = int(max_price_val)
                max_text = "undefined" if max_price == 0 else str(max_price)
            
            search_filter = f"priceRange:{min_text},{max_text};"
        else:
            search_filter = ""

        data = {
            "pageNumber": page,
            "keyword": keyword,
            "fromFilter": kwargs.get("fromFilter", use_filter),
            "rowsPerPage": 30,
            "sortValue": "desc",  # 排序方向：desc 降序 / asc 升序
            "sortField": "create",  # 排序字段：create 按发布时间
            "customDistance": "",
            "gps": "",
            "customGps": "",
            # 兼容闲鱼 PC 搜索的筛选字段
            "propValueStr": {"searchFilter": search_filter} if search_filter else {},
            "searchReqFromPage": "pcSearch",
            "extraFilterValue": "{}",
            "userPositionJson": "{}"
        }
        
        # 添加可选参数（向后兼容）
        # if "sortType" in kwargs:
        #     # sortType 映射到 sortValue
        #     data["sortValue"] = kwargs["sortType"]
        # if "publishDay" in kwargs:
        #     data["publishDay"] = kwargs["publishDay"]
        
        result = await self.request(GoofishAPIs.SEARCH, data)
        return result
    
    async def get_address_list(self) -> Dict[str, Any]:
        """获取收货地址列表"""
        result = await self.request(GoofishAPIs.ADDRESS_LIST, {})
        return result
    
    # ⚠️ 注意：silentHasLogin API只是用来判断登录状态的，不能用来重新登录
    # 如果需要重新登录，应该使用浏览器刷新页面（refresh_and_check_login）
    # 这个方法已废弃，统一使用 has_login() 来判断登录状态
    
    async def get_user_id(self, force_check_login: bool = False) -> Dict[str, Any]:
        """
        获取登录用户ID
        Args:
            force_check_login: 是否强制检查登录（即使标记为封禁也调用API检查真实状态）
        Returns:
            {
                "api": "mtop.taobao.idlemessage.pc.loginuser.get",
                "data": {
                    "needDecryptKeys": [],
                    "needDecryptKeysV2": [],
                    "userId": 670846218
                },
                "ret": ["SUCCESS::调用成功"],
                "v": "1.0"
            }
        """
        result = await self.request(GoofishAPIs.LOGIN_USER_GET, {}, force_check_login=force_check_login)
        return result
    
    async def has_login(self) -> Dict[str, Any]:
        """
        检测登录状态（通过silentHasLogin API检测）
        
        调用 https://passport.goofish.com/newlogin/silentHasLogin.do
        判断cookie是否登录有效
        
        Returns:
            {
                "success": bool,  # API请求是否成功
                "is_logged_in": bool,  # 是否已登录（processFinished=true）
                "data": dict,  # API返回的data
                "error": str (可选)  # 错误信息（titleMsg）
            }
        """
        try:
            logger.debug("🔍 调用silentHasLogin API检测登录状态...")
            
            # 构建请求URL
            url = (f"{self.PASSPORT_HOST}/newlogin/silentHasLogin.do"
                   f"?documentReferer=https://www.goofish.com/"
                   f"&appName=xianyu"
                   f"&appEntrance=xianyu_sdkSilent"
                   f"&fromSite=0"
                   f"&ltl=true")
            
            # 构建请求头
            headers = {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0",
                "sec-ch-ua": "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Microsoft Edge\";v=\"138\"",
                "Referer": "https://www.goofish.com/",
                "Pragma": "no-cache",
                "Accept": "*/*",
                "Origin": "https://www.goofish.com",
                "Dnt": "1",
                "Sec-Fetch-Dest": "empty",
                "Sec-Fetch-Site": "same-site",
                "Sec-Fetch-Mode": "cors",
                "Sec-Ch-Ua-Mobile": "?0",
                "Sec-Ch-Ua-Platform": "\"Windows\""
            }
            
            # 获取session并发送POST请求（body为空）
            session = await self.get_session()
            async with session.post(url, headers=headers, data=None, timeout=aiohttp.ClientTimeout(total=10)) as response:
                if response.status != 200:
                    error_msg = f"HTTP {response.status}"
                    logger.warning(f"❌ 检测登录状态失败：{error_msg}")
                    return {
                        "success": False,
                        "is_logged_in": False,
                        "data": {},
                        "error": error_msg
                    }
                
                result_text = await response.text()
                logger.debug(f"has_login响应: {result_text[:200]}")
                
                try:
                    result = json.loads(result_text)
                except json.JSONDecodeError:
                    logger.warning("❌ 检测登录状态失败：响应不是JSON格式")
                    return {
                        "success": False,
                        "is_logged_in": False,
                        "data": {},
                        "error": "响应格式错误"
                    }
                
                # ✅ 解析响应：只有 processFinished=true 才表示已登录
                content = result.get("content", {})
                if not content:
                    logger.warning("❌ 检测登录状态失败：响应中没有content字段")
                    return {
                        "success": False,
                        "is_logged_in": False,
                        "data": {},
                        "error": "响应格式错误"
                    }
                
                data = content.get("data", {})
                process_finished = data.get("processFinished", False)
                
                if process_finished:
                    # ✅ 已登录：processFinished=true 表示登录完成
                    logger.debug("✅ 登录状态有效（processFinished=true）")
                    return {
                        "success": True,
                        "is_logged_in": True,
                        "data": data,
                        "error": None
                    }
                else:
                    # ✅ 未登录：其他所有情况都是未登录（包括titleMsg存在或不存在）
                    title_msg = data.get("titleMsg", "登录状态无效")
                    logger.debug(f"⚠️  登录状态无效：{title_msg}")
                    return {
                        "success": True,  # API请求成功，只是登录状态无效
                        "is_logged_in": False,
                        "data": data,
                        "error": title_msg
                    }
                    
        except Exception as e:
            logger.error(f"检测登录状态失败: {e}")
            return {
                "success": False,
                "is_logged_in": False,
                "data": {},
                "error": str(e)
            }
    
    async def get_item_detail(self, item_id: str) -> Dict[str, Any]:
        """
        获取商品详情
        Args:
            item_id: 商品ID
        """
        data = {"itemId": item_id}
        result = await self.request(GoofishAPIs.ITEM_DETAIL, data)
        return result
    
    async def render_order(self, item_id: str) -> Dict[str, Any]:
        """
        渲染订单（获取订单信息）
        Args:
            item_id: 商品ID
        """
        data = {"itemId": item_id}
        result = await self.request(GoofishAPIs.ORDER_RENDER, data)
        return result
    
    async def create_order(
        self,
        item_buy_info_list: list,
        **kwargs
    ) -> Dict[str, Any]:
        """
        创建订单（按照C#逻辑修复）
        Args:
            item_buy_info_list: itemBuyInfo数组（从OrderRender返回的commonData.itemBuyInfo）
                            每个itemBuyInfo应包含：itemId, deliverId, price, skuId等字段
        Note:
            C#代码中：CreateOrder API的参数是 { @params = JsonConvert.SerializeObject(itemBuyInfo数组) }
            Python中：params是参数名（不需要@符号），值是JSON序列化的itemBuyInfo数组
        """
        import json
        # ✅ 按照C#逻辑：将itemBuyInfo数组序列化为JSON字符串
        params_json = json.dumps(item_buy_info_list, ensure_ascii=False, separators=(',', ':'))
        
        # ✅ 按照C#逻辑：参数名为 params（C#中用@params是因为params是关键字，Python中用params即可）
        data = {
            "params": params_json,
            **kwargs
        }
        result = await self.request(GoofishAPIs.CREATE_ORDER, data)
        return result
    
    async def cancel_order(
        self,
        order_id: str,
        reason: str = "不想买了"
    ) -> Dict[str, Any]:
        """
        取消订单（参考C#代码参数格式）
        Args:
            order_id: 订单ID（bizOrderId）
            reason: 取消原因
                - 不想买了
                - 信息填写错误，重新拍
                - 同城见面交易
                - 卖家缺货
                - 其他原因
        Returns:
            API响应
        """
        # ✅ 参考C#代码：参数格式为 { tid = order.bizOrderId, closeReason = reason, order.bizOrderId }
        data = {
            "tid": order_id,  # ✅ 订单ID（对应C#的order.bizOrderId）
            "closeReason": reason,  # ✅ 取消原因（对应C#的closeReason）
            "bizOrderId": order_id  # ✅ 订单ID（对应C#的order.bizOrderId，重复字段）
        }
        result = await self.request(GoofishAPIs.CLOSE_ORDER, data)
        return result
    
    async def query_blacklist(self, session_id: str) -> Dict[str, Any]:
        """
        查询黑名单状态
        Args:
            session_id: 会话ID（从conversation_id提取，如 "55659170628"）
        Returns:
            API响应，包含 isInBlack 字段
        """
        data = {"sessionId": session_id}
        result = await self.request(GoofishAPIs.BLACKLIST_QUERY, data)
        return result
    
    async def add_blacklist(self, session_id: str) -> Dict[str, Any]:
        """
        添加黑名单
        Args:
            session_id: 会话ID（从conversation_id提取，如 "55659170628"）
        Returns:
            API响应
        """
        data = {"sessionId": session_id}
        result = await self.request(GoofishAPIs.BLACKLIST_ADD, data)
        return result
    
    async def remove_blacklist(self, session_id: str) -> Dict[str, Any]:
        """
        移除黑名单
        Args:
            session_id: 会话ID（从conversation_id提取，如 "55659170628"）
        Returns:
            API响应
        """
        data = {"sessionId": session_id}
        result = await self.request(GoofishAPIs.BLACKLIST_REMOVE, data)
        return result
    
    async def load_emoji_list(self) -> Dict[str, Any]:
        """
        加载表情包列表（仅请求API，不做缓存）
        
        该方法仅负责从闲鱼API获取表情包数据。
        缓存、调度等逻辑由控制端（Backend）负责。
        
        Returns:
            {
                "success": True/False,
                "items": [...],  # 表情包列表
                "alias_map": {...},  # 表情别名映射（用于快速查找）
                "error": "错误信息"  # 仅失败时存在
            }
        """
        try:
            logger.info("正在请求表情包API...")
            data = {}  # 空数据
            result = await self.request(GoofishAPIs.EMOJI_LOAD, data)
            
            # 检查响应
            ret_codes = result.get("ret", [])
            if ret_codes and any(code.startswith("SUCCESS") for code in ret_codes):
                emoji_data = result.get("data", {})
                items = emoji_data.get("items", [])
                
                if items:
                    # 构建表情别名映射（用于快速查找）
                    alias_map = {}
                    for emoji in items:
                        icon_alias = emoji.get("iconAlias", "")
                        if icon_alias:
                            alias_map[icon_alias] = emoji
                    
                    logger.success(f"✅ 表情包加载成功（共{len(items)}个表情）")
                    return {
                        "success": True,
                        "items": items,
                        "alias_map": alias_map
                    }
                else:
                    logger.warning("表情包API返回成功，但数据为空")
                    return {
                        "success": False,
                        "items": [],
                        "alias_map": {},
                        "error": "表情包数据为空"
                    }
            else:
                # API返回失败
                error_msg = result.get("msg", "未知错误")
                error_code = ret_codes[0] if ret_codes else "NO_CODE"
                
                # 检查是否需要登录
                if "SESSION_EXPIRED" in str(error_code) or "LOGIN" in str(error_code):
                    logger.warning(f"表情包API需要登录：{error_code} - {error_msg}")
                    return {
                        "success": False,
                        "items": [],
                        "alias_map": {},
                        "error": f"需要登录才能获取表情包：{error_msg}",
                        "need_login": True
                    }
                else:
                    logger.error(f"表情包API请求失败：{error_code} - {error_msg}")
                    return {
                        "success": False,
                        "items": [],
                        "alias_map": {},
                        "error": f"API请求失败：{error_msg}"
                    }
        
        except Exception as e:
            logger.exception(f"加载表情包异常: {e}")
            return {
                "success": False,
                "items": [],
                "alias_map": {},
                "error": str(e)
            }
    
    async def set_login_keep(self, keep: bool = True) -> Dict[str, Any]:
        """
        设置是否保存登录信息（记住登录状态）
        
        ⚠️ 在设置前会先检查 hasLongTokenLogin，如果已经开启自动登录且 keep=True，则跳过设置
        
        Args:
            keep: True=保存登录信息, False=不保存
        Returns:
            API响应
        """
        # ✅ 如果 keep=True，先检查是否已经开启自动登录
        if keep:
            try:
                status_result = await self.query_login_keep_status()
                if status_result.get("success") and status_result.get("has_long_token_login"):
                    logger.debug("✅ 已开启自动登录（hasLongTokenLogin=true），跳过设置")
                    return {
                        "code": 0,
                        "msgCode": "H-AC-SUCCESS-0000",
                        "msgInfo": "已开启自动登录",
                        "returnValue": {
                            "actionType": "TOAST",
                            "displayMessage": "已开启自动登录",
                            "status": 200,
                            "occurError": False
                        },
                        "success": True
                    }
            except Exception as e:
                logger.debug(f"查询登录保存状态失败（继续设置）: {e}")
        
        # 使用主session发送请求
        session = await self.get_session()
        
        # API URL
        url = f"{self.PASSPORT_HOST}/ac/account/setLoginSettings.do"
        params = {
            "fromSite": "77",
            "appName": "xianyu",
            "bizEntrance": "web"
        }
        
        # status: 0=保存, 1=不保存
        data = {"status": "0" if keep else "1"}
        
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0",
            "Sec-Ch-Ua": "\"Chromium\";v=\"138\", \"Not?A_Brand\";v=\"8\"",
            "Referer": "https://www.goofish.com/",
            "Pragma": "no-cache",
            "Accept": "application/json, text/plain, */*",
            "Origin": "https://www.goofish.com",
            "Dnt": "1",
            "Sec-Fetch-Dest": "empty",
            "Sec-Fetch-Site": "same-site",
            "Sec-Fetch-Mode": "cors",
            "Sec-Ch-Ua-Mobile": "?0",
            "Sec-Ch-Ua-Platform": "\"Windows\"",
            "Content-Type": "application/x-www-form-urlencoded"
        }
        
        try:
            async with session.post(url, params=params, data=data, headers=headers, timeout=aiohttp.ClientTimeout(total=10)) as resp:
                result = await resp.json()
                logger.info(f"设置保存登录状态: keep={keep}, result={result}")
                
                # ✅ 从响应头中提取Set-Cookie并同步登录保持相关cookie
                # 登录保持相关cookie列表（根据能自动登录的账号cookie对比）
                login_keep_cookies = [
                    "havana_lgc2_77",
                    "_hvn_lgc_",
                    "havana_lgc_exp",
                    "last_u_xianyu_xianyu_sdkSilent",
                    "last_cc"  # 也可能是必需的
                ]
                
                synced_cookies = []
                
                # 从响应头的Set-Cookie中解析cookie
                set_cookie_headers = resp.headers.getall('Set-Cookie', [])
                for set_cookie_header in set_cookie_headers:
                    # Set-Cookie格式: "name=value; Domain=.goofish.com; Path=/; Expires=..."
                    # 解析cookie名称和值
                    try:
                        # 取第一个分号前的部分（cookie名称=值）
                        cookie_part = set_cookie_header.split(';')[0].strip()
                        if '=' in cookie_part:
                            cookie_name, cookie_value = cookie_part.split('=', 1)
                            if cookie_name in login_keep_cookies:
                                # _sync_cookie会自动从页面获取domain
                                await self._sync_cookie(cookie_name, cookie_value)
                                synced_cookies.append(cookie_name)
                                logger.info(f"✅ 已同步登录保持cookie: {cookie_name}")
                    except Exception as e:
                        logger.debug(f"解析Set-Cookie失败: {set_cookie_header[:50]}...: {e}")
                
                # 如果响应头中没有，尝试从session的cookie_jar中提取（aiohttp会自动添加）
                if not synced_cookies and not session.closed:
                    try:
                        # cookie_jar是一个可迭代对象
                        for cookie_obj in session.cookie_jar:
                            cookie_name = cookie_obj.key
                            if cookie_name in login_keep_cookies:
                                cookie_value = cookie_obj.value
                                await self._sync_cookie(cookie_name, cookie_value)
                                synced_cookies.append(cookie_name)
                                logger.info(f"✅ 已从cookie_jar同步登录保持cookie: {cookie_name}")
                    except Exception as e:
                        logger.debug(f"从cookie_jar提取cookie失败: {e}")
                
                if synced_cookies:
                    logger.success(f"✅ 已同步登录保持相关cookie: {', '.join(synced_cookies)}")
                else:
                    logger.debug(f"⚠️  响应中未找到登录保持相关cookie，可能已存在或未返回")
            
            return result
        except Exception as e:
            logger.error(f"设置保存登录状态失败: {e}")
            raise
    
    async def query_login_keep_status(self) -> Dict[str, Any]:
        """
        查询登录保存状态（判断是否开启自动登录）
        
        API: https://passport.goofish.com/ac/account/queryLoginSettings.do
        
        Returns:
            {
                "success": bool,  # API请求是否成功
                "has_long_token_login": bool,  # 是否已开启自动登录（hasLongTokenLogin）
                "can_open_long_login": bool,  # 是否可以设置自动登录（canOpenLongLogin）
                "data": dict  # 完整响应数据
            }
        """
        try:
            url = f"{self.PASSPORT_HOST}/ac/account/queryLoginSettings.do"
            params = {
                "fromSite": "77",
                "appName": "xianyu",
                "bizEntrance": "web"
            }
            
            # 构建请求头（按照API文档）
            headers = {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0",
                "Sec-Ch-Ua": "\"Chromium\";v=\"138\", \"Not?A_Brand\";v=\"8\"",
                "Referer": "https://www.goofish.com/",
                "Pragma": "no-cache",
                "Accept": "application/json, text/plain, */*",
                "Origin": "https://www.goofish.com",
                "Dnt": "1",
                "Sec-Fetch-Dest": "empty",
                "Sec-Fetch-Site": "same-site",
                "Sec-Fetch-Mode": "cors",
                "Sec-Ch-Ua-Mobile": "?0",
                "Sec-Ch-Ua-Platform": "\"Windows\""
            }
            
            session = await self.get_session()
            
            # 发送POST请求（body为空）
            async with session.post(url, params=params, headers=headers, data=None, timeout=aiohttp.ClientTimeout(total=10)) as response:
                if response.status != 200:
                    error_msg = f"HTTP {response.status}"
                    logger.warning(f"❌ 查询登录保存状态失败：{error_msg}")
                    return {
                        "success": False,
                        "has_long_token_login": False,
                        "can_open_long_login": False,
                        "data": {},
                        "error": error_msg
                    }
                
                result = await response.json()
                logger.debug(f"查询登录保存状态响应: {result}")
                
                # ✅ 解析响应：returnValue.hasLongTokenLogin 表示是否已开启自动登录
                if result.get("success") and result.get("code") == 0:
                    return_value = result.get("returnValue", {})
                    has_long_token_login = return_value.get("hasLongTokenLogin", False)
                    can_open_long_login = return_value.get("canOpenLongLogin", False)
                    
                    return {
                        "success": True,
                        "has_long_token_login": has_long_token_login,
                        "can_open_long_login": can_open_long_login,
                        "data": return_value,
                        "error": None
                    }
                else:
                    error_msg = result.get("msg", "未知错误")
                    logger.warning(f"❌ 查询登录保存状态失败：{error_msg}")
                    return {
                        "success": False,
                        "has_long_token_login": False,
                        "can_open_long_login": False,
                        "data": result,
                        "error": error_msg
                    }
                    
        except Exception as e:
            logger.error(f"查询登录保存状态异常: {e}")
            return {
                "success": False,
                "has_long_token_login": False,
                "can_open_long_login": False,
                "data": {},
                "error": str(e)
            }


# ========== 辅助函数 ========== #

def parse_emoji_text(text: str, alias_map: Dict[str, Any], format_type: str = "html") -> str:
    """
    解析文本中的表情标记，将其替换为表情图片或保留文本
    
    Args:
        text: 原始文本，如 "运费50太离谱了[捧脸哭]"
        alias_map: 表情别名映射（从 load_emoji_list 返回）
        format_type: 输出格式
            - "html": 输出 HTML <img> 标签
            - "markdown": 输出 Markdown ![alt](url) 格式
            - "url": 只返回图片URL列表（不替换文本）
    
    Returns:
        处理后的文本或URL列表
    
    Example:
        >>> alias_map = {"[捧脸哭]": {"iconShowUrl": "https://...", "iconName": "捧脸哭"}}
        >>> parse_emoji_text("运费50太离谱了[捧脸哭]", alias_map, "html")
        '运费50太离谱了<img src="https://..." alt="捧脸哭" class="emoji" />'
    """
    import re
    
    if not text or not alias_map:
        return text
    
    # 匹配表情标记 [xxx]
    pattern = r'\[([^\[\]]+?)\]'
    
    if format_type == "url":
        # 只返回URL列表
        emoji_urls = []
        for match in re.finditer(pattern, text):
            emoji_text = f"[{match.group(1)}]"
            if emoji_text in alias_map:
                emoji_info = alias_map[emoji_text]
                # 优先使用 iconShowUrl（动图），其次使用 iconUrl（静态图）
                emoji_url = emoji_info.get("iconShowUrl") or emoji_info.get("iconUrl")
                if emoji_url:
                    emoji_urls.append(emoji_url)
        return emoji_urls
    
    # 替换文本中的表情
    def replace_emoji(match):
        emoji_text = f"[{match.group(1)}]"
        if emoji_text in alias_map:
            emoji_info = alias_map[emoji_text]
            emoji_name = emoji_info.get("iconName", match.group(1))
            # 优先使用 iconShowUrl（动图），其次使用 iconUrl（静态图）
            emoji_url = emoji_info.get("iconShowUrl") or emoji_info.get("iconUrl")
            
            if emoji_url:
                if format_type == "html":
                    return f'<img src="{emoji_url}" alt="{emoji_name}" class="emoji" title="{emoji_name}" style="display:inline-block;width:20px;height:20px;vertical-align:middle;" />'
                elif format_type == "markdown":
                    return f"![{emoji_name}]({emoji_url})"
        
        # 如果找不到，保留原文本
        return emoji_text
    
    result = re.sub(pattern, replace_emoji, text)
    return result


# ========== 导出 ========== #
__all__ = [
    'GoofishAPIClient',
    'GoofishAPIs',
    'ResponseCode',
    'parse_emoji_text'
]

